1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27 package org.apache.http.impl.client.cache;
28
29 import java.io.IOException;
30 import java.util.Arrays;
31 import java.util.Date;
32 import java.util.HashMap;
33 import java.util.HashSet;
34 import java.util.Map;
35 import java.util.Set;
36
37 import org.apache.commons.logging.Log;
38 import org.apache.commons.logging.LogFactory;
39 import org.apache.http.Header;
40 import org.apache.http.HttpHost;
41 import org.apache.http.HttpRequest;
42 import org.apache.http.HttpResponse;
43 import org.apache.http.HttpStatus;
44 import org.apache.http.HttpVersion;
45 import org.apache.http.client.cache.HeaderConstants;
46 import org.apache.http.client.cache.HttpCacheEntry;
47 import org.apache.http.client.cache.HttpCacheInvalidator;
48 import org.apache.http.client.cache.HttpCacheStorage;
49 import org.apache.http.client.cache.HttpCacheUpdateCallback;
50 import org.apache.http.client.cache.HttpCacheUpdateException;
51 import org.apache.http.client.cache.Resource;
52 import org.apache.http.client.cache.ResourceFactory;
53 import org.apache.http.client.methods.CloseableHttpResponse;
54 import org.apache.http.client.methods.HttpRequestWrapper;
55 import org.apache.http.entity.ByteArrayEntity;
56 import org.apache.http.message.BasicHttpResponse;
57 import org.apache.http.protocol.HTTP;
58
59 class BasicHttpCache implements HttpCache {
60 private static final Set<String> safeRequestMethods = new HashSet<String>(
61 Arrays.asList(HeaderConstants.HEAD_METHOD,
62 HeaderConstants.GET_METHOD, HeaderConstants.OPTIONS_METHOD,
63 HeaderConstants.TRACE_METHOD));
64
65 private final CacheKeyGenerator uriExtractor;
66 private final ResourceFactory resourceFactory;
67 private final long maxObjectSizeBytes;
68 private final CacheEntryUpdater cacheEntryUpdater;
69 private final CachedHttpResponseGenerator responseGenerator;
70 private final HttpCacheInvalidator cacheInvalidator;
71 private final HttpCacheStorage storage;
72
73 private final Log log = LogFactory.getLog(getClass());
74
75 public BasicHttpCache(
76 final ResourceFactory resourceFactory,
77 final HttpCacheStorage storage,
78 final CacheConfig config,
79 final CacheKeyGenerator uriExtractor,
80 final HttpCacheInvalidator cacheInvalidator) {
81 this.resourceFactory = resourceFactory;
82 this.uriExtractor = uriExtractor;
83 this.cacheEntryUpdater = new CacheEntryUpdater(resourceFactory);
84 this.maxObjectSizeBytes = config.getMaxObjectSize();
85 this.responseGenerator = new CachedHttpResponseGenerator();
86 this.storage = storage;
87 this.cacheInvalidator = cacheInvalidator;
88 }
89
90 public BasicHttpCache(
91 final ResourceFactory resourceFactory,
92 final HttpCacheStorage storage,
93 final CacheConfig config,
94 final CacheKeyGenerator uriExtractor) {
95 this( resourceFactory, storage, config, uriExtractor,
96 new CacheInvalidator(uriExtractor, storage));
97 }
98
99 public BasicHttpCache(
100 final ResourceFactory resourceFactory,
101 final HttpCacheStorage storage,
102 final CacheConfig config) {
103 this( resourceFactory, storage, config, new CacheKeyGenerator());
104 }
105
106 public BasicHttpCache(final CacheConfig config) {
107 this(new HeapResourceFactory(), new BasicHttpCacheStorage(config), config);
108 }
109
110 public BasicHttpCache() {
111 this(CacheConfig.DEFAULT);
112 }
113
114 @Override
115 public void flushCacheEntriesFor(final HttpHost host, final HttpRequest request)
116 throws IOException {
117 if (!safeRequestMethods.contains(request.getRequestLine().getMethod())) {
118 final String uri = uriExtractor.getURI(host, request);
119 storage.removeEntry(uri);
120 }
121 }
122
123 @Override
124 public void flushInvalidatedCacheEntriesFor(final HttpHost host, final HttpRequest request, final HttpResponse response) {
125 if (!safeRequestMethods.contains(request.getRequestLine().getMethod())) {
126 cacheInvalidator.flushInvalidatedCacheEntries(host, request, response);
127 }
128 }
129
130 void storeInCache(
131 final HttpHost target, final HttpRequest request, final HttpCacheEntry entry) throws IOException {
132 if (entry.hasVariants()) {
133 storeVariantEntry(target, request, entry);
134 } else {
135 storeNonVariantEntry(target, request, entry);
136 }
137 }
138
139 void storeNonVariantEntry(
140 final HttpHost target, final HttpRequest req, final HttpCacheEntry entry) throws IOException {
141 final String uri = uriExtractor.getURI(target, req);
142 storage.putEntry(uri, entry);
143 }
144
145 void storeVariantEntry(
146 final HttpHost target,
147 final HttpRequest req,
148 final HttpCacheEntry entry) throws IOException {
149 final String parentURI = uriExtractor.getURI(target, req);
150 final String variantURI = uriExtractor.getVariantURI(target, req, entry);
151 storage.putEntry(variantURI, entry);
152
153 final HttpCacheUpdateCallbackateCallback.html#HttpCacheUpdateCallback">HttpCacheUpdateCallback callback = new HttpCacheUpdateCallback() {
154
155 @Override
156 public HttpCacheEntryttpCacheEntry.html#HttpCacheEntry">HttpCacheEntry update(final HttpCacheEntry existing) throws IOException {
157 return doGetUpdatedParentEntry(
158 req.getRequestLine().getUri(), existing, entry,
159 uriExtractor.getVariantKey(req, entry),
160 variantURI);
161 }
162
163 };
164
165 try {
166 storage.updateEntry(parentURI, callback);
167 } catch (final HttpCacheUpdateException e) {
168 log.warn("Could not update key [" + parentURI + "]", e);
169 }
170 }
171
172 @Override
173 public void reuseVariantEntryFor(final HttpHost target, final HttpRequest req,
174 final Variant variant) throws IOException {
175 final String parentCacheKey = uriExtractor.getURI(target, req);
176 final HttpCacheEntry entry = variant.getEntry();
177 final String variantKey = uriExtractor.getVariantKey(req, entry);
178 final String variantCacheKey = variant.getCacheKey();
179
180 final HttpCacheUpdateCallbackateCallback.html#HttpCacheUpdateCallback">HttpCacheUpdateCallback callback = new HttpCacheUpdateCallback() {
181 @Override
182 public HttpCacheEntryttpCacheEntry.html#HttpCacheEntry">HttpCacheEntry update(final HttpCacheEntry existing)
183 throws IOException {
184 return doGetUpdatedParentEntry(req.getRequestLine().getUri(),
185 existing, entry, variantKey, variantCacheKey);
186 }
187 };
188
189 try {
190 storage.updateEntry(parentCacheKey, callback);
191 } catch (final HttpCacheUpdateException e) {
192 log.warn("Could not update key [" + parentCacheKey + "]", e);
193 }
194 }
195
196 boolean isIncompleteResponse(final HttpResponse resp, final Resource resource) {
197 final int status = resp.getStatusLine().getStatusCode();
198 if (status != HttpStatus.SC_OK
199 && status != HttpStatus.SC_PARTIAL_CONTENT) {
200 return false;
201 }
202 final Header hdr = resp.getFirstHeader(HTTP.CONTENT_LEN);
203 if (hdr == null) {
204 return false;
205 }
206 final int contentLength;
207 try {
208 contentLength = Integer.parseInt(hdr.getValue());
209 } catch (final NumberFormatException nfe) {
210 return false;
211 }
212 if (resource == null) {
213 return false;
214 }
215 return (resource.length() < contentLength);
216 }
217
218 CloseableHttpResponse generateIncompleteResponseError(
219 final HttpResponse response, final Resource resource) {
220 final Integer contentLength = Integer.valueOf(response.getFirstHeader(HTTP.CONTENT_LEN).getValue());
221 final HttpResponse error =
222 new BasicHttpResponse(HttpVersion.HTTP_1_1, HttpStatus.SC_BAD_GATEWAY, "Bad Gateway");
223 error.setHeader("Content-Type","text/plain;charset=UTF-8");
224 final String msg = String.format("Received incomplete response " +
225 "with Content-Length %d but actual body length %d",
226 contentLength, resource.length());
227 final byte[] msgBytes = msg.getBytes();
228 error.setHeader("Content-Length", Integer.toString(msgBytes.length));
229 error.setEntity(new ByteArrayEntity(msgBytes));
230 return Proxies.enhanceResponse(error);
231 }
232
233 HttpCacheEntry doGetUpdatedParentEntry(
234 final String requestId,
235 final HttpCacheEntry existing,
236 final HttpCacheEntry entry,
237 final String variantKey,
238 final String variantCacheKey) throws IOException {
239 HttpCacheEntry src = existing;
240 if (src == null) {
241 src = entry;
242 }
243
244 Resource resource = null;
245 if (src.getResource() != null) {
246 resource = resourceFactory.copy(requestId, src.getResource());
247 }
248 final Map<String,String> variantMap = new HashMap<String,String>(src.getVariantMap());
249 variantMap.put(variantKey, variantCacheKey);
250 return new HttpCacheEntry(
251 src.getRequestDate(),
252 src.getResponseDate(),
253 src.getStatusLine(),
254 src.getAllHeaders(),
255 resource,
256 variantMap,
257 src.getRequestMethod());
258 }
259
260 @Override
261 public HttpCacheEntry updateCacheEntry(final HttpHost target, final HttpRequest request,
262 final HttpCacheEntry stale, final HttpResponse originResponse,
263 final Date requestSent, final Date responseReceived) throws IOException {
264 final HttpCacheEntry updatedEntry = cacheEntryUpdater.updateCacheEntry(
265 request.getRequestLine().getUri(),
266 stale,
267 requestSent,
268 responseReceived,
269 originResponse);
270 storeInCache(target, request, updatedEntry);
271 return updatedEntry;
272 }
273
274 @Override
275 public HttpCacheEntry updateVariantCacheEntry(final HttpHost target, final HttpRequest request,
276 final HttpCacheEntry stale, final HttpResponse originResponse,
277 final Date requestSent, final Date responseReceived, final String cacheKey) throws IOException {
278 final HttpCacheEntry updatedEntry = cacheEntryUpdater.updateCacheEntry(
279 request.getRequestLine().getUri(),
280 stale,
281 requestSent,
282 responseReceived,
283 originResponse);
284 storage.putEntry(cacheKey, updatedEntry);
285 return updatedEntry;
286 }
287
288 @Override
289 public HttpResponse cacheAndReturnResponse(final HttpHost host, final HttpRequest request,
290 final HttpResponse originResponse, final Date requestSent, final Date responseReceived)
291 throws IOException {
292 return cacheAndReturnResponse(host, request,
293 Proxies.enhanceResponse(originResponse), requestSent,
294 responseReceived);
295 }
296
297 @Override
298 public CloseableHttpResponse cacheAndReturnResponse(
299 final HttpHost host,
300 final HttpRequest request,
301 final CloseableHttpResponse originResponse,
302 final Date requestSent,
303 final Date responseReceived) throws IOException {
304
305 boolean closeOriginResponse = true;
306 final SizeLimitedResponseReader responseReader = getResponseReader(request, originResponse);
307 try {
308 responseReader.readResponse();
309
310 if (responseReader.isLimitReached()) {
311 closeOriginResponse = false;
312 return responseReader.getReconstructedResponse();
313 }
314
315 final Resource resource = responseReader.getResource();
316 if (isIncompleteResponse(originResponse, resource)) {
317 return generateIncompleteResponseError(originResponse, resource);
318 }
319
320 final HttpCacheEntryHttpCacheEntry.html#HttpCacheEntry">HttpCacheEntry entry = new HttpCacheEntry(
321 requestSent,
322 responseReceived,
323 originResponse.getStatusLine(),
324 originResponse.getAllHeaders(),
325 resource,
326 request.getRequestLine().getMethod());
327 storeInCache(host, request, entry);
328 return responseGenerator.generateResponse(HttpRequestWrapper.wrap(request, host), entry);
329 } finally {
330 if (closeOriginResponse) {
331 originResponse.close();
332 }
333 }
334 }
335
336 SizeLimitedResponseReader getResponseReader(final HttpRequest request,
337 final CloseableHttpResponse backEndResponse) {
338 return new SizeLimitedResponseReader(
339 resourceFactory, maxObjectSizeBytes, request, backEndResponse);
340 }
341
342 @Override
343 public HttpCacheEntry getCacheEntry(final HttpHost host, final HttpRequest request) throws IOException {
344 final HttpCacheEntry root = storage.getEntry(uriExtractor.getURI(host, request));
345 if (root == null) {
346 return null;
347 }
348 if (!root.hasVariants()) {
349 return root;
350 }
351 final String variantCacheKey = root.getVariantMap().get(uriExtractor.getVariantKey(request, root));
352 if (variantCacheKey == null) {
353 return null;
354 }
355 return storage.getEntry(variantCacheKey);
356 }
357
358 @Override
359 public void flushInvalidatedCacheEntriesFor(final HttpHost host,
360 final HttpRequest request) throws IOException {
361 cacheInvalidator.flushInvalidatedCacheEntries(host, request);
362 }
363
364 @Override
365 public Map<String, Variant> getVariantCacheEntriesWithEtags(final HttpHost host, final HttpRequest request)
366 throws IOException {
367 final Map<String,Variant> variants = new HashMap<String,Variant>();
368 final HttpCacheEntry root = storage.getEntry(uriExtractor.getURI(host, request));
369 if (root == null || !root.hasVariants()) {
370 return variants;
371 }
372 for(final Map.Entry<String, String> variant : root.getVariantMap().entrySet()) {
373 final String variantKey = variant.getKey();
374 final String variantCacheKey = variant.getValue();
375 addVariantWithEtag(variantKey, variantCacheKey, variants);
376 }
377 return variants;
378 }
379
380 private void addVariantWithEtag(final String variantKey,
381 final String variantCacheKey, final Map<String, Variant> variants)
382 throws IOException {
383 final HttpCacheEntry entry = storage.getEntry(variantCacheKey);
384 if (entry == null) {
385 return;
386 }
387 final Header etagHeader = entry.getFirstHeader(HeaderConstants.ETAG);
388 if (etagHeader == null) {
389 return;
390 }
391 variants.put(etagHeader.getValue(), new Variant(variantKey, variantCacheKey, entry));
392 }
393
394 }