1
17 package okhttp3.internal.cache;
18
19 import java.io.IOException;
20 import javax.annotation.Nullable;
21 import okhttp3.Headers;
22 import okhttp3.Interceptor;
23 import okhttp3.Protocol;
24 import okhttp3.Request;
25 import okhttp3.Response;
26 import okhttp3.internal.Internal;
27 import okhttp3.internal.Util;
28 import okhttp3.internal.http.ExchangeCodec;
29 import okhttp3.internal.http.HttpHeaders;
30 import okhttp3.internal.http.HttpMethod;
31 import okhttp3.internal.http.RealResponseBody;
32 import okio.Buffer;
33 import okio.BufferedSink;
34 import okio.BufferedSource;
35 import okio.Okio;
36 import okio.Sink;
37 import okio.Source;
38 import okio.Timeout;
39
40 import static java.net.HttpURLConnection.HTTP_NOT_MODIFIED;
41 import static java.util.concurrent.TimeUnit.MILLISECONDS;
42 import static okhttp3.internal.Util.closeQuietly;
43 import static okhttp3.internal.Util.discard;
44
45
46 public final class CacheInterceptor implements Interceptor {
47 final @Nullable InternalCache cache;
48
49 public CacheInterceptor(@Nullable InternalCache cache) {
50 this.cache = cache;
51 }
52
53 @Override public Response intercept(Chain chain) throws IOException {
54 Response cacheCandidate = cache != null
55 ? cache.get(chain.request())
56 : null;
57
58 long now = System.currentTimeMillis();
59
60 CacheStrategy strategy = new CacheStrategy.Factory(now, chain.request(), cacheCandidate).get();
61 Request networkRequest = strategy.networkRequest;
62 Response cacheResponse = strategy.cacheResponse;
63
64 if (cache != null) {
65 cache.trackResponse(strategy);
66 }
67
68 if (cacheCandidate != null && cacheResponse == null) {
69 closeQuietly(cacheCandidate.body());
70 }
71
72
73 if (networkRequest == null && cacheResponse == null) {
74 return new Response.Builder()
75 .request(chain.request())
76 .protocol(Protocol.HTTP_1_1)
77 .code(504)
78 .message("Unsatisfiable Request (only-if-cached)")
79 .body(Util.EMPTY_RESPONSE)
80 .sentRequestAtMillis(-1L)
81 .receivedResponseAtMillis(System.currentTimeMillis())
82 .build();
83 }
84
85
86 if (networkRequest == null) {
87 return cacheResponse.newBuilder()
88 .cacheResponse(stripBody(cacheResponse))
89 .build();
90 }
91
92 Response networkResponse = null;
93 try {
94 networkResponse = chain.proceed(networkRequest);
95 } finally {
96
97 if (networkResponse == null && cacheCandidate != null) {
98 closeQuietly(cacheCandidate.body());
99 }
100 }
101
102
103 if (cacheResponse != null) {
104 if (networkResponse.code() == HTTP_NOT_MODIFIED) {
105 Response response = cacheResponse.newBuilder()
106 .headers(combine(cacheResponse.headers(), networkResponse.headers()))
107 .sentRequestAtMillis(networkResponse.sentRequestAtMillis())
108 .receivedResponseAtMillis(networkResponse.receivedResponseAtMillis())
109 .cacheResponse(stripBody(cacheResponse))
110 .networkResponse(stripBody(networkResponse))
111 .build();
112 networkResponse.body().close();
113
114
115
116 cache.trackConditionalCacheHit();
117 cache.update(cacheResponse, response);
118 return response;
119 } else {
120 closeQuietly(cacheResponse.body());
121 }
122 }
123
124 Response response = networkResponse.newBuilder()
125 .cacheResponse(stripBody(cacheResponse))
126 .networkResponse(stripBody(networkResponse))
127 .build();
128
129 if (cache != null) {
130 if (HttpHeaders.hasBody(response) && CacheStrategy.isCacheable(response, networkRequest)) {
131
132 CacheRequest cacheRequest = cache.put(response);
133 return cacheWritingResponse(cacheRequest, response);
134 }
135
136 if (HttpMethod.invalidatesCache(networkRequest.method())) {
137 try {
138 cache.remove(networkRequest);
139 } catch (IOException ignored) {
140
141 }
142 }
143 }
144
145 return response;
146 }
147
148 private static Response stripBody(Response response) {
149 return response != null && response.body() != null
150 ? response.newBuilder().body(null).build()
151 : response;
152 }
153
154
159 private Response cacheWritingResponse(final CacheRequest cacheRequest, Response response)
160 throws IOException {
161
162 if (cacheRequest == null) return response;
163 Sink cacheBodyUnbuffered = cacheRequest.body();
164 if (cacheBodyUnbuffered == null) return response;
165
166 final BufferedSource source = response.body().source();
167 final BufferedSink cacheBody = Okio.buffer(cacheBodyUnbuffered);
168
169 Source cacheWritingSource = new Source() {
170 boolean cacheRequestClosed;
171
172 @Override public long read(Buffer sink, long byteCount) throws IOException {
173 long bytesRead;
174 try {
175 bytesRead = source.read(sink, byteCount);
176 } catch (IOException e) {
177 if (!cacheRequestClosed) {
178 cacheRequestClosed = true;
179 cacheRequest.abort();
180 }
181 throw e;
182 }
183
184 if (bytesRead == -1) {
185 if (!cacheRequestClosed) {
186 cacheRequestClosed = true;
187 cacheBody.close();
188 }
189 return -1;
190 }
191
192 sink.copyTo(cacheBody.buffer(), sink.size() - bytesRead, bytesRead);
193 cacheBody.emitCompleteSegments();
194 return bytesRead;
195 }
196
197 @Override public Timeout timeout() {
198 return source.timeout();
199 }
200
201 @Override public void close() throws IOException {
202 if (!cacheRequestClosed
203 && !discard(this, ExchangeCodec.DISCARD_STREAM_TIMEOUT_MILLIS, MILLISECONDS)) {
204 cacheRequestClosed = true;
205 cacheRequest.abort();
206 }
207 source.close();
208 }
209 };
210
211 String contentType = response.header("Content-Type");
212 long contentLength = response.body().contentLength();
213 return response.newBuilder()
214 .body(new RealResponseBody(contentType, contentLength, Okio.buffer(cacheWritingSource)))
215 .build();
216 }
217
218
219 private static Headers combine(Headers cachedHeaders, Headers networkHeaders) {
220 Headers.Builder result = new Headers.Builder();
221
222 for (int i = 0, size = cachedHeaders.size(); i < size; i++) {
223 String fieldName = cachedHeaders.name(i);
224 String value = cachedHeaders.value(i);
225 if ("Warning".equalsIgnoreCase(fieldName) && value.startsWith("1")) {
226 continue;
227 }
228 if (isContentSpecificHeader(fieldName)
229 || !isEndToEnd(fieldName)
230 || networkHeaders.get(fieldName) == null) {
231 Internal.instance.addLenient(result, fieldName, value);
232 }
233 }
234
235 for (int i = 0, size = networkHeaders.size(); i < size; i++) {
236 String fieldName = networkHeaders.name(i);
237 if (!isContentSpecificHeader(fieldName) && isEndToEnd(fieldName)) {
238 Internal.instance.addLenient(result, fieldName, networkHeaders.value(i));
239 }
240 }
241
242 return result.build();
243 }
244
245
249 static boolean isEndToEnd(String fieldName) {
250 return !"Connection".equalsIgnoreCase(fieldName)
251 && !"Keep-Alive".equalsIgnoreCase(fieldName)
252 && !"Proxy-Authenticate".equalsIgnoreCase(fieldName)
253 && !"Proxy-Authorization".equalsIgnoreCase(fieldName)
254 && !"TE".equalsIgnoreCase(fieldName)
255 && !"Trailers".equalsIgnoreCase(fieldName)
256 && !"Transfer-Encoding".equalsIgnoreCase(fieldName)
257 && !"Upgrade".equalsIgnoreCase(fieldName);
258 }
259
260
264 static boolean isContentSpecificHeader(String fieldName) {
265 return "Content-Length".equalsIgnoreCase(fieldName)
266 || "Content-Encoding".equalsIgnoreCase(fieldName)
267 || "Content-Type".equalsIgnoreCase(fieldName);
268 }
269 }
270