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.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
46
47
48
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
123 maxstale = 0;
124 }
125 }
126 }
127 }
128 }
129 return maxstale;
130 }
131
132
133
134
135
136
137
138
139
140
141
142
143
144
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
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
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
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
260
261
262
263 public boolean isConditional(final HttpRequest request) {
264 return hasSupportedEtagValidator(request) || hasSupportedLastModifiedValidator(request);
265 }
266
267
268
269
270
271
272
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
310
311
312
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
334
335
336
337
338
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 }