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.util.Date;
30  
31  import org.apache.commons.logging.Log;
32  import org.apache.commons.logging.LogFactory;
33  import org.apache.http.Header;
34  import org.apache.http.HeaderElement;
35  import org.apache.http.HttpHost;
36  import org.apache.http.HttpRequest;
37  import org.apache.http.HttpStatus;
38  import org.apache.http.annotation.Contract;
39  import org.apache.http.annotation.ThreadingBehavior;
40  import org.apache.http.client.cache.HeaderConstants;
41  import org.apache.http.client.cache.HttpCacheEntry;
42  import org.apache.http.client.utils.DateUtils;
43  
44  /**
45   * Determines whether a given {@link HttpCacheEntry} is suitable to be
46   * used as a response for a given {@link HttpRequest}.
47   *
48   * @since 4.1
49   */
50  @Contract(threading = ThreadingBehavior.IMMUTABLE_CONDITIONAL)
51  class CachedResponseSuitabilityChecker {
52  
53      private final Log log = LogFactory.getLog(getClass());
54  
55      private final boolean sharedCache;
56      private final boolean useHeuristicCaching;
57      private final float heuristicCoefficient;
58      private final long heuristicDefaultLifetime;
59      private final CacheValidityPolicy validityStrategy;
60  
61      CachedResponseSuitabilityChecker(final CacheValidityPolicy validityStrategy,
62              final CacheConfig config) {
63          super();
64          this.validityStrategy = validityStrategy;
65          this.sharedCache = config.isSharedCache();
66          this.useHeuristicCaching = config.isHeuristicCachingEnabled();
67          this.heuristicCoefficient = config.getHeuristicCoefficient();
68          this.heuristicDefaultLifetime = config.getHeuristicDefaultLifetime();
69      }
70  
71      CachedResponseSuitabilityChecker(final CacheConfig config) {
72          this(new CacheValidityPolicy(), config);
73      }
74  
75      private boolean isFreshEnough(final HttpCacheEntry entry, final HttpRequest request, final Date now) {
76          if (validityStrategy.isResponseFresh(entry, now)) {
77              return true;
78          }
79          if (useHeuristicCaching &&
80                  validityStrategy.isResponseHeuristicallyFresh(entry, now, heuristicCoefficient, heuristicDefaultLifetime)) {
81              return true;
82          }
83          if (originInsistsOnFreshness(entry)) {
84              return false;
85          }
86          final long maxstale = getMaxStale(request);
87          if (maxstale == -1) {
88              return false;
89          }
90          return (maxstale > validityStrategy.getStalenessSecs(entry, now));
91      }
92  
93      private boolean originInsistsOnFreshness(final HttpCacheEntry entry) {
94          if (validityStrategy.mustRevalidate(entry)) {
95              return true;
96          }
97          if (!sharedCache) {
98              return false;
99          }
100         return validityStrategy.proxyRevalidate(entry) ||
101             validityStrategy.hasCacheControlDirective(entry, "s-maxage");
102     }
103 
104     private long getMaxStale(final HttpRequest request) {
105         long maxstale = -1;
106         for(final Header h : request.getHeaders(HeaderConstants.CACHE_CONTROL)) {
107             for(final HeaderElement elt : h.getElements()) {
108                 if (HeaderConstants.CACHE_CONTROL_MAX_STALE.equals(elt.getName())) {
109                     if ((elt.getValue() == null || "".equals(elt.getValue().trim()))
110                             && maxstale == -1) {
111                         maxstale = Long.MAX_VALUE;
112                     } else {
113                         try {
114                             long val = Long.parseLong(elt.getValue());
115                             if (val < 0) {
116                                 val = 0;
117                             }
118                             if (maxstale == -1 || val < maxstale) {
119                                 maxstale = val;
120                             }
121                         } catch (final NumberFormatException nfe) {
122                             // err on the side of preserving semantic transparency
123                             maxstale = 0;
124                         }
125                     }
126                 }
127             }
128         }
129         return maxstale;
130     }
131 
132     /**
133      * Determine if I can utilize a {@link HttpCacheEntry} to respond to the given
134      * {@link HttpRequest}
135      *
136      * @param host
137      *            {@link HttpHost}
138      * @param request
139      *            {@link HttpRequest}
140      * @param entry
141      *            {@link HttpCacheEntry}
142      * @param now
143      *            Right now in time
144      * @return boolean yes/no answer
145      */
146     public boolean canCachedResponseBeUsed(final HttpHost host, final HttpRequest request, final HttpCacheEntry entry, final Date now) {
147         if (!isFreshEnough(entry, request, now)) {
148             log.trace("Cache entry was not fresh enough");
149             return false;
150         }
151 
152         if (isGet(request) && !validityStrategy.contentLengthHeaderMatchesActualLength(entry)) {
153             log.debug("Cache entry Content-Length and header information do not match");
154             return false;
155         }
156 
157         if (hasUnsupportedConditionalHeaders(request)) {
158             log.debug("Request contained conditional headers we don't handle");
159             return false;
160         }
161 
162         if (!isConditional(request) && entry.getStatusCode() == HttpStatus.SC_NOT_MODIFIED) {
163             return false;
164         }
165 
166         if (isConditional(request) && !allConditionalsMatch(request, entry, now)) {
167             return false;
168         }
169 
170         if (hasUnsupportedCacheEntryForGet(request, entry)) {
171             log.debug("HEAD response caching enabled but the cache entry does not contain a " +
172                       "request method, entity or a 204 response");
173             return false;
174         }
175 
176         for (final Header ccHdr : request.getHeaders(HeaderConstants.CACHE_CONTROL)) {
177             for (final HeaderElement elt : ccHdr.getElements()) {
178                 if (HeaderConstants.CACHE_CONTROL_NO_CACHE.equals(elt.getName())) {
179                     log.trace("Response contained NO CACHE directive, cache was not suitable");
180                     return false;
181                 }
182 
183                 if (HeaderConstants.CACHE_CONTROL_NO_STORE.equals(elt.getName())) {
184                     log.trace("Response contained NO STORE directive, cache was not suitable");
185                     return false;
186                 }
187 
188                 if (HeaderConstants.CACHE_CONTROL_MAX_AGE.equals(elt.getName())) {
189                     try {
190                         final int maxage = Integer.parseInt(elt.getValue());
191                         if (validityStrategy.getCurrentAgeSecs(entry, now) > maxage) {
192                             log.trace("Response from cache was NOT suitable due to max age");
193                             return false;
194                         }
195                     } catch (final NumberFormatException ex) {
196                         // err conservatively
197                         log.debug("Response from cache was malformed" + ex.getMessage());
198                         return false;
199                     }
200                 }
201 
202                 if (HeaderConstants.CACHE_CONTROL_MAX_STALE.equals(elt.getName())) {
203                     try {
204                         final int maxstale = Integer.parseInt(elt.getValue());
205                         if (validityStrategy.getFreshnessLifetimeSecs(entry) > maxstale) {
206                             log.trace("Response from cache was not suitable due to Max stale freshness");
207                             return false;
208                         }
209                     } catch (final NumberFormatException ex) {
210                         // err conservatively
211                         log.debug("Response from cache was malformed: " + ex.getMessage());
212                         return false;
213                     }
214                 }
215 
216                 if (HeaderConstants.CACHE_CONTROL_MIN_FRESH.equals(elt.getName())) {
217                     try {
218                         final long minfresh = Long.parseLong(elt.getValue());
219                         if (minfresh < 0L) {
220                             return false;
221                         }
222                         final long age = validityStrategy.getCurrentAgeSecs(entry, now);
223                         final long freshness = validityStrategy.getFreshnessLifetimeSecs(entry);
224                         if (freshness - age < minfresh) {
225                             log.trace("Response from cache was not suitable due to min fresh " +
226                                     "freshness requirement");
227                             return false;
228                         }
229                     } catch (final NumberFormatException ex) {
230                         // err conservatively
231                         log.debug("Response from cache was malformed: " + ex.getMessage());
232                         return false;
233                     }
234                 }
235             }
236         }
237 
238         log.trace("Response from cache was suitable");
239         return true;
240     }
241 
242     private boolean isGet(final HttpRequest request) {
243         return request.getRequestLine().getMethod().equals(HeaderConstants.GET_METHOD);
244     }
245 
246     private boolean entryIsNotA204Response(final HttpCacheEntry entry) {
247         return entry.getStatusCode() != HttpStatus.SC_NO_CONTENT;
248     }
249 
250     private boolean cacheEntryDoesNotContainMethodAndEntity(final HttpCacheEntry entry) {
251         return entry.getRequestMethod() == null && entry.getResource() == null;
252     }
253 
254     private boolean hasUnsupportedCacheEntryForGet(final HttpRequest request, final HttpCacheEntry entry) {
255         return isGet(request) && cacheEntryDoesNotContainMethodAndEntity(entry) && entryIsNotA204Response(entry);
256     }
257 
258     /**
259      * Is this request the type of conditional request we support?
260      * @param request The current httpRequest being made
261      * @return {@code true} if the request is supported
262      */
263     public boolean isConditional(final HttpRequest request) {
264         return hasSupportedEtagValidator(request) || hasSupportedLastModifiedValidator(request);
265     }
266 
267     /**
268      * Check that conditionals that are part of this request match
269      * @param request The current httpRequest being made
270      * @param entry the cache entry
271      * @param now right NOW in time
272      * @return {@code true} if the request matches all conditionals
273      */
274     public boolean allConditionalsMatch(final HttpRequest request, final HttpCacheEntry entry, final Date now) {
275         final boolean hasEtagValidator = hasSupportedEtagValidator(request);
276         final boolean hasLastModifiedValidator = hasSupportedLastModifiedValidator(request);
277 
278         final boolean etagValidatorMatches = (hasEtagValidator) && etagValidatorMatches(request, entry);
279         final boolean lastModifiedValidatorMatches = (hasLastModifiedValidator) && lastModifiedValidatorMatches(request, entry, now);
280 
281         if ((hasEtagValidator && hasLastModifiedValidator)
282             && !(etagValidatorMatches && lastModifiedValidatorMatches)) {
283             return false;
284         } else if (hasEtagValidator && !etagValidatorMatches) {
285             return false;
286         }
287 
288         if (hasLastModifiedValidator && !lastModifiedValidatorMatches) {
289             return false;
290         }
291         return true;
292     }
293 
294     private boolean hasUnsupportedConditionalHeaders(final HttpRequest request) {
295         return (request.getFirstHeader(HeaderConstants.IF_RANGE) != null
296                 || request.getFirstHeader(HeaderConstants.IF_MATCH) != null
297                 || hasValidDateField(request, HeaderConstants.IF_UNMODIFIED_SINCE));
298     }
299 
300     private boolean hasSupportedEtagValidator(final HttpRequest request) {
301         return request.containsHeader(HeaderConstants.IF_NONE_MATCH);
302     }
303 
304     private boolean hasSupportedLastModifiedValidator(final HttpRequest request) {
305         return hasValidDateField(request, HeaderConstants.IF_MODIFIED_SINCE);
306     }
307 
308     /**
309      * Check entry against If-None-Match
310      * @param request The current httpRequest being made
311      * @param entry the cache entry
312      * @return boolean does the etag validator match
313      */
314     private boolean etagValidatorMatches(final HttpRequest request, final HttpCacheEntry entry) {
315         final Header etagHeader = entry.getFirstHeader(HeaderConstants.ETAG);
316         final String etag = (etagHeader != null) ? etagHeader.getValue() : null;
317         final Header[] ifNoneMatch = request.getHeaders(HeaderConstants.IF_NONE_MATCH);
318         if (ifNoneMatch != null) {
319             for (final Header h : ifNoneMatch) {
320                 for (final HeaderElement elt : h.getElements()) {
321                     final String reqEtag = elt.toString();
322                     if (("*".equals(reqEtag) && etag != null)
323                             || reqEtag.equals(etag)) {
324                         return true;
325                     }
326                 }
327             }
328         }
329         return false;
330     }
331 
332     /**
333      * Check entry against If-Modified-Since, if If-Modified-Since is in the future it is invalid as per
334      * http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html
335      * @param request The current httpRequest being made
336      * @param entry the cache entry
337      * @param now right NOW in time
338      * @return  boolean Does the last modified header match
339      */
340     private boolean lastModifiedValidatorMatches(final HttpRequest request, final HttpCacheEntry entry, final Date now) {
341         final Header lastModifiedHeader = entry.getFirstHeader(HeaderConstants.LAST_MODIFIED);
342         Date lastModified = null;
343         if (lastModifiedHeader != null) {
344             lastModified = DateUtils.parseDate(lastModifiedHeader.getValue());
345         }
346         if (lastModified == null) {
347             return false;
348         }
349 
350         for (final Header h : request.getHeaders(HeaderConstants.IF_MODIFIED_SINCE)) {
351             final Date ifModifiedSince = DateUtils.parseDate(h.getValue());
352             if (ifModifiedSince != null) {
353                 if (ifModifiedSince.after(now) || lastModified.after(ifModifiedSince)) {
354                     return false;
355                 }
356             }
357         }
358         return true;
359     }
360 
361     private boolean hasValidDateField(final HttpRequest request, final String headerName) {
362         for(final Header h : request.getHeaders(headerName)) {
363             final Date date = DateUtils.parseDate(h.getValue());
364             return date != null;
365         }
366         return false;
367     }
368 }