1
2
3
4
5
6
7
8
9
10
11
12
13
14
15 package org.htmlunit;
16
17 import java.io.Serializable;
18 import java.net.URL;
19 import java.util.Collections;
20 import java.util.Date;
21 import java.util.HashMap;
22 import java.util.Map;
23 import java.util.regex.Matcher;
24 import java.util.regex.Pattern;
25
26 import org.htmlunit.cssparser.dom.CSSStyleSheetImpl;
27 import org.htmlunit.http.HttpUtils;
28 import org.htmlunit.util.HeaderUtils;
29 import org.htmlunit.util.UrlUtils;
30
31
32
33
34
35
36
37
38
39
40
41
42
43 public class Cache implements Serializable {
44
45
46 private int maxSize_ = 40;
47
48 private static final Pattern DATE_HEADER_PATTERN = Pattern.compile("-?\\d+");
49 static final long DELAY = 10 * org.apache.commons.lang3.time.DateUtils.MILLIS_PER_MINUTE;
50
51
52 private static final double TEN_PERCENT_OF_MILLISECONDS_IN_SECONDS = 0.0001;
53
54
55
56
57
58
59
60
61 private final Map<String, Entry> entries_ = Collections.synchronizedMap(new HashMap<>(maxSize_));
62
63
64
65
66 private static class Entry implements Comparable<Entry>, Serializable {
67 private final String key_;
68 private final WebResponse response_;
69 private final Object value_;
70 private long lastAccess_;
71 private final long createdAt_;
72
73 Entry(final String key, final WebResponse response, final Object value) {
74 key_ = key;
75 response_ = response;
76 value_ = value;
77 createdAt_ = System.currentTimeMillis();
78 lastAccess_ = createdAt_;
79 }
80
81
82
83
84 @Override
85 public int compareTo(final Entry other) {
86 return Long.compare(lastAccess_, other.lastAccess_);
87 }
88
89
90
91
92 @Override
93 public boolean equals(final Object obj) {
94 return obj instanceof Entry && lastAccess_ == ((Entry) obj).lastAccess_;
95 }
96
97
98
99
100 @Override
101 public int hashCode() {
102 return ((Long) lastAccess_).hashCode();
103 }
104
105
106
107
108 public void touch() {
109 lastAccess_ = System.currentTimeMillis();
110 }
111
112
113
114
115
116
117
118 boolean isStillFresh(final long now) {
119 return Cache.isWithinCacheWindow(response_, now, createdAt_);
120 }
121 }
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138 static boolean isWithinCacheWindow(final WebResponse response, final long now, final long createdAt) {
139 long freshnessLifetime = 0;
140 if (!HeaderUtils.containsPrivate(response) && HeaderUtils.containsSMaxage(response)) {
141
142 freshnessLifetime = HeaderUtils.sMaxage(response);
143 }
144 else if (HeaderUtils.containsMaxAge(response)) {
145
146 freshnessLifetime = HeaderUtils.maxAge(response);
147 }
148 else if (response.getResponseHeaderValue(HttpHeader.EXPIRES) != null) {
149 final Date expires = parseDateHeader(response, HttpHeader.EXPIRES);
150 if (expires != null) {
151
152 return expires.getTime() - now > DELAY;
153 }
154 }
155 else if (response.getResponseHeaderValue(HttpHeader.LAST_MODIFIED) != null) {
156 final Date lastModified = parseDateHeader(response, HttpHeader.LAST_MODIFIED);
157 if (lastModified != null) {
158 freshnessLifetime = (long) ((createdAt - lastModified.getTime())
159 * TEN_PERCENT_OF_MILLISECONDS_IN_SECONDS);
160 }
161 }
162 return now - createdAt < freshnessLifetime * org.apache.commons.lang3.time.DateUtils.MILLIS_PER_SECOND;
163 }
164
165
166
167
168
169
170
171
172
173
174
175 public boolean cacheIfPossible(final WebRequest request, final WebResponse response, final Object toCache) {
176 if (isCacheable(request, response)) {
177 final URL url = request.getUrl();
178 if (url == null) {
179 return false;
180 }
181
182 final Entry entry = new Entry(UrlUtils.normalize(url), response, toCache);
183 entries_.put(entry.key_, entry);
184 deleteOverflow();
185 return true;
186 }
187
188 return false;
189 }
190
191
192
193
194
195
196
197
198
199
200
201
202 public void cache(final String css, final CSSStyleSheetImpl styleSheet) {
203 final Entry entry = new Entry(css, null, styleSheet);
204 entries_.put(entry.key_, entry);
205 deleteOverflow();
206 }
207
208
209
210
211 protected void deleteOverflow() {
212 synchronized (entries_) {
213 while (entries_.size() > maxSize_) {
214 final Entry oldestEntry = Collections.min(entries_.values());
215 entries_.remove(oldestEntry.key_);
216 if (oldestEntry.response_ != null) {
217 oldestEntry.response_.cleanUp();
218 }
219 }
220 }
221 }
222
223
224
225
226
227
228
229
230 protected boolean isCacheable(final WebRequest request, final WebResponse response) {
231 return HttpMethod.GET == response.getWebRequest().getHttpMethod()
232 && UrlUtils.URL_ABOUT_BLANK != request.getUrl()
233 && isCacheableContent(response);
234 }
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254 protected boolean isCacheableContent(final WebResponse response) {
255 if (HeaderUtils.containsNoStore(response)) {
256 return false;
257 }
258
259 final long now = getCurrentTimestamp();
260 return isWithinCacheWindow(response, now, now);
261 }
262
263
264
265
266
267 protected long getCurrentTimestamp() {
268 return System.currentTimeMillis();
269 }
270
271
272
273
274
275
276
277
278
279 protected static Date parseDateHeader(final WebResponse response, final String headerName) {
280 final String value = response.getResponseHeaderValue(headerName);
281 if (value == null) {
282 return null;
283 }
284 final Matcher matcher = DATE_HEADER_PATTERN.matcher(value);
285 if (matcher.matches()) {
286 return new Date();
287 }
288 return HttpUtils.parseDate(value);
289 }
290
291
292
293
294
295
296
297
298
299
300
301 public WebResponse getCachedResponse(final WebRequest request) {
302 final Entry cachedEntry = getCacheEntry(request);
303 if (cachedEntry == null) {
304 return null;
305 }
306 return cachedEntry.response_;
307 }
308
309
310
311
312
313
314
315
316
317
318
319 public Object getCachedObject(final WebRequest request) {
320 final Entry cachedEntry = getCacheEntry(request);
321 if (cachedEntry == null) {
322 return null;
323 }
324 return cachedEntry.value_;
325 }
326
327 private Entry getCacheEntry(final WebRequest request) {
328 if (HttpMethod.GET != request.getHttpMethod()) {
329 return null;
330 }
331
332 final URL url = request.getUrl();
333 if (url == null) {
334 return null;
335 }
336
337 final String normalizedUrl = UrlUtils.normalize(url);
338 final Entry cachedEntry = entries_.get(normalizedUrl);
339 if (cachedEntry == null) {
340 return null;
341 }
342
343 if (cachedEntry.isStillFresh(getCurrentTimestamp())) {
344 synchronized (entries_) {
345 cachedEntry.touch();
346 }
347 return cachedEntry;
348 }
349 entries_.remove(UrlUtils.normalize(url));
350 return null;
351 }
352
353
354
355
356
357
358
359
360 public CSSStyleSheetImpl getCachedStyleSheet(final String css) {
361 final Entry cachedEntry = entries_.get(css);
362 if (cachedEntry == null) {
363 return null;
364 }
365 synchronized (entries_) {
366 cachedEntry.touch();
367 }
368 return (CSSStyleSheetImpl) cachedEntry.value_;
369 }
370
371
372
373
374
375
376
377 public int getMaxSize() {
378 return maxSize_;
379 }
380
381
382
383
384
385
386
387 public void setMaxSize(final int maxSize) {
388 if (maxSize < 0) {
389 throw new IllegalArgumentException("Illegal value for maxSize: " + maxSize);
390 }
391 maxSize_ = maxSize;
392 deleteOverflow();
393 }
394
395
396
397
398
399
400 public int getSize() {
401 return entries_.size();
402 }
403
404
405
406
407 public void clear() {
408 synchronized (entries_) {
409 for (final Entry entry : entries_.values()) {
410 if (entry.response_ != null) {
411 entry.response_.cleanUp();
412 }
413 }
414 entries_.clear();
415 }
416 }
417
418
419
420
421 public void clearOutdated() {
422 synchronized (entries_) {
423 final long now = getCurrentTimestamp();
424
425 entries_.entrySet().removeIf(entry -> entry.getValue().response_ == null
426 || !entry.getValue().isStillFresh(now));
427 }
428 }
429 }