View Javadoc
1   /*
2    * ====================================================================
3    * Licensed to the Apache Software Foundation (ASF) under one
4    * or more contributor license agreements.  See the NOTICE file
5    * distributed with this work for additional information
6    * regarding copyright ownership.  The ASF licenses this file
7    * to you under the Apache License, Version 2.0 (the
8    * "License"); you may not use this file except in compliance
9    * with the License.  You may obtain a copy of the License at
10   *
11   *   http://www.apache.org/licenses/LICENSE-2.0
12   *
13   * Unless required by applicable law or agreed to in writing,
14   * software distributed under the License is distributed on an
15   * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
16   * KIND, either express or implied.  See the License for the
17   * specific language governing permissions and limitations
18   * under the License.
19   * ====================================================================
20   *
21   * This software consists of voluntary contributions made by many
22   * individuals on behalf of the Apache Software Foundation.  For more
23   * information on the Apache Software Foundation, please see
24   * <http://www.apache.org/>.
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 }