Commit | Line | Data |
---|---|---|
8816d2f7 NR |
1 | package be.nikiroo.utils; |
2 | ||
3 | import java.io.File; | |
8816d2f7 NR |
4 | import java.io.FileNotFoundException; |
5 | import java.io.IOException; | |
6 | import java.io.InputStream; | |
7 | import java.net.URL; | |
8 | import java.util.Date; | |
9 | ||
8e76f6ab NR |
10 | import be.nikiroo.utils.streams.MarkableFileInputStream; |
11 | ||
8816d2f7 NR |
12 | /** |
13 | * A generic cache system, with special support for {@link URL}s. | |
14 | * <p> | |
15 | * This cache also manages timeout information. | |
16 | * | |
17 | * @author niki | |
18 | */ | |
19 | public class Cache { | |
20 | private File dir; | |
21 | private long tooOldChanging; | |
22 | private long tooOldStable; | |
530d4062 | 23 | private TraceHandler tracer = new TraceHandler(); |
8816d2f7 | 24 | |
e8aa5bf9 NR |
25 | /** |
26 | * Only for inheritance. | |
27 | */ | |
28 | protected Cache() { | |
29 | } | |
30 | ||
8816d2f7 NR |
31 | /** |
32 | * Create a new {@link Cache} object. | |
33 | * | |
34 | * @param dir | |
35 | * the directory to use as cache | |
36 | * @param hoursChanging | |
37 | * the number of hours after which a cached file that is thought | |
38 | * to change ~often is considered too old (or -1 for | |
39 | * "never too old") | |
40 | * @param hoursStable | |
41 | * the number of hours after which a cached file that is thought | |
42 | * to change rarely is considered too old (or -1 for | |
43 | * "never too old") | |
44 | * | |
45 | * @throws IOException | |
46 | * in case of I/O error | |
47 | */ | |
48 | public Cache(File dir, int hoursChanging, int hoursStable) | |
49 | throws IOException { | |
50 | this.dir = dir; | |
0988831f NR |
51 | this.tooOldChanging = 1000L * 60 * 60 * hoursChanging; |
52 | this.tooOldStable = 1000L * 60 * 60 * hoursStable; | |
8816d2f7 NR |
53 | |
54 | if (dir != null && !dir.exists()) { | |
55 | dir.mkdirs(); | |
56 | } | |
57 | ||
58 | if (dir == null || !dir.exists()) { | |
59 | throw new IOException("Cannot create the cache directory: " | |
60 | + (dir == null ? "null" : dir.getAbsolutePath())); | |
61 | } | |
62 | } | |
63 | ||
530d4062 NR |
64 | /** |
65 | * The traces handler for this {@link Cache}. | |
66 | * | |
67 | * @return the traces handler | |
68 | */ | |
69 | public TraceHandler getTraceHandler() { | |
70 | return tracer; | |
71 | } | |
72 | ||
73 | /** | |
74 | * The traces handler for this {@link Cache}. | |
75 | * | |
76 | * @param tracer | |
77 | * the new traces handler | |
78 | */ | |
79 | public void setTraceHandler(TraceHandler tracer) { | |
80500544 NR |
80 | if (tracer == null) { |
81 | tracer = new TraceHandler(false, false, false); | |
82 | } | |
83 | ||
530d4062 NR |
84 | this.tracer = tracer; |
85 | } | |
86 | ||
e8aa5bf9 NR |
87 | /** |
88 | * Check the resource to see if it is in the cache. | |
89 | * | |
90 | * @param uniqueID | |
91 | * the resource to check | |
92 | * @param allowTooOld | |
93 | * allow files even if they are considered too old | |
94 | * @param stable | |
95 | * a stable file (that dones't change too often) -- parameter | |
96 | * used to check if the file is too old to keep or not | |
97 | * | |
98 | * @return TRUE if it is | |
99 | * | |
100 | */ | |
101 | public boolean check(String uniqueID, boolean allowTooOld, boolean stable) { | |
102 | return check(getCached(uniqueID), allowTooOld, stable); | |
103 | } | |
104 | ||
8816d2f7 NR |
105 | /** |
106 | * Check the resource to see if it is in the cache. | |
107 | * | |
108 | * @param url | |
109 | * the resource to check | |
110 | * @param allowTooOld | |
111 | * allow files even if they are considered too old | |
112 | * @param stable | |
113 | * a stable file (that dones't change too often) -- parameter | |
114 | * used to check if the file is too old to keep or not | |
115 | * | |
116 | * @return TRUE if it is | |
117 | * | |
118 | */ | |
119 | public boolean check(URL url, boolean allowTooOld, boolean stable) { | |
e8aa5bf9 NR |
120 | return check(getCached(url), allowTooOld, stable); |
121 | } | |
122 | ||
123 | /** | |
124 | * Check the resource to see if it is in the cache. | |
125 | * | |
126 | * @param cached | |
127 | * the resource to check | |
128 | * @param allowTooOld | |
129 | * allow files even if they are considered too old | |
130 | * @param stable | |
131 | * a stable file (that dones't change too often) -- parameter | |
132 | * used to check if the file is too old to keep or not | |
133 | * | |
134 | * @return TRUE if it is | |
135 | * | |
136 | */ | |
137 | private boolean check(File cached, boolean allowTooOld, boolean stable) { | |
138 | if (cached.exists() && cached.isFile()) { | |
e704a414 NR |
139 | if (!allowTooOld && isOld(cached, stable)) { |
140 | if (!cached.delete()) { | |
141 | tracer.error("Cannot delete temporary file: " | |
142 | + cached.getAbsolutePath()); | |
143 | } | |
144 | } else { | |
8816d2f7 NR |
145 | return true; |
146 | } | |
147 | } | |
148 | ||
149 | return false; | |
150 | } | |
151 | ||
152 | /** | |
153 | * Clean the cache (delete the cached items). | |
154 | * | |
155 | * @param onlyOld | |
156 | * only clean the files that are considered too old for a stable | |
157 | * resource | |
158 | * | |
159 | * @return the number of cleaned items | |
160 | */ | |
161 | public int clean(boolean onlyOld) { | |
82fcfcde NR |
162 | long ms = System.currentTimeMillis(); |
163 | ||
164 | tracer.trace("Cleaning cache from old files..."); | |
165 | ||
223aa0d4 | 166 | int num = clean(onlyOld, dir, -1); |
82fcfcde | 167 | |
223aa0d4 NR |
168 | tracer.trace(num + "cache items cleaned in " |
169 | + (System.currentTimeMillis() - ms) + " ms"); | |
82fcfcde NR |
170 | |
171 | return num; | |
172 | } | |
173 | ||
174 | /** | |
223aa0d4 | 175 | * Clean the cache (delete the cached items) in the given cache directory. |
82fcfcde NR |
176 | * |
177 | * @param onlyOld | |
178 | * only clean the files that are considered too old for stable | |
179 | * resources | |
180 | * @param cacheDir | |
181 | * the cache directory to clean | |
223aa0d4 NR |
182 | * @param limit |
183 | * stop after limit files deleted, or -1 for unlimited | |
82fcfcde NR |
184 | * |
185 | * @return the number of cleaned items | |
186 | */ | |
223aa0d4 | 187 | private int clean(boolean onlyOld, File cacheDir, int limit) { |
8816d2f7 | 188 | int num = 0; |
0988831f NR |
189 | File[] files = cacheDir.listFiles(); |
190 | if (files != null) { | |
191 | for (File file : files) { | |
223aa0d4 NR |
192 | if (limit >= 0 && num >= limit) { |
193 | return num; | |
194 | } | |
195 | ||
0988831f | 196 | if (file.isDirectory()) { |
223aa0d4 NR |
197 | num += clean(onlyOld, file, limit); |
198 | file.delete(); // only if empty | |
0988831f NR |
199 | } else { |
200 | if (!onlyOld || isOld(file, true)) { | |
201 | if (file.delete()) { | |
202 | num++; | |
203 | } else { | |
204 | tracer.error("Cannot delete temporary file: " | |
205 | + file.getAbsolutePath()); | |
206 | } | |
8816d2f7 NR |
207 | } |
208 | } | |
209 | } | |
210 | } | |
211 | ||
212 | return num; | |
213 | } | |
214 | ||
215 | /** | |
216 | * Open a resource from the cache if it exists. | |
217 | * | |
218 | * @param uniqueID | |
219 | * the unique ID | |
220 | * @param allowTooOld | |
221 | * allow files even if they are considered too old | |
222 | * @param stable | |
223 | * a stable file (that dones't change too often) -- parameter | |
224 | * used to check if the file is too old to keep or not | |
225 | * | |
226 | * @return the opened resource if found, NULL if not | |
8816d2f7 NR |
227 | */ |
228 | public InputStream load(String uniqueID, boolean allowTooOld, boolean stable) { | |
229 | return load(getCached(uniqueID), allowTooOld, stable); | |
230 | } | |
231 | ||
232 | /** | |
233 | * Open a resource from the cache if it exists. | |
234 | * | |
235 | * @param url | |
236 | * the resource to open | |
237 | * @param allowTooOld | |
238 | * allow files even if they are considered too old | |
239 | * @param stable | |
f6e8d60d NR |
240 | * a stable file (that doesn't change too often) -- parameter |
241 | * used to check if the file is too old to keep or not in the | |
242 | * cache | |
8816d2f7 NR |
243 | * |
244 | * @return the opened resource if found, NULL if not | |
8816d2f7 | 245 | */ |
2ee6c205 | 246 | public InputStream load(URL url, boolean allowTooOld, boolean stable) { |
8816d2f7 NR |
247 | return load(getCached(url), allowTooOld, stable); |
248 | } | |
249 | ||
250 | /** | |
251 | * Open a resource from the cache if it exists. | |
252 | * | |
2ee6c205 | 253 | * @param cached |
8816d2f7 NR |
254 | * the resource to open |
255 | * @param allowTooOld | |
256 | * allow files even if they are considered too old | |
257 | * @param stable | |
258 | * a stable file (that dones't change too often) -- parameter | |
259 | * used to check if the file is too old to keep or not | |
260 | * | |
261 | * @return the opened resource if found, NULL if not | |
8816d2f7 NR |
262 | */ |
263 | private InputStream load(File cached, boolean allowTooOld, boolean stable) { | |
80500544 NR |
264 | if (cached.exists() && cached.isFile() |
265 | && (allowTooOld || !isOld(cached, stable))) { | |
8816d2f7 | 266 | try { |
7194ac50 | 267 | return new MarkableFileInputStream(cached); |
8816d2f7 NR |
268 | } catch (FileNotFoundException e) { |
269 | return null; | |
270 | } | |
271 | } | |
272 | ||
273 | return null; | |
274 | } | |
275 | ||
276 | /** | |
277 | * Save the given resource to the cache. | |
278 | * | |
279 | * @param in | |
280 | * the input data | |
281 | * @param uniqueID | |
282 | * a unique ID used to locate the cached resource | |
283 | * | |
59654e2a NR |
284 | * @return the number of bytes written |
285 | * | |
8816d2f7 NR |
286 | * @throws IOException |
287 | * in case of I/O error | |
288 | */ | |
59654e2a | 289 | public long save(InputStream in, String uniqueID) throws IOException { |
8816d2f7 NR |
290 | File cached = getCached(uniqueID); |
291 | cached.getParentFile().mkdirs(); | |
59654e2a | 292 | return save(in, cached); |
8816d2f7 NR |
293 | } |
294 | ||
295 | /** | |
296 | * Save the given resource to the cache. | |
297 | * | |
298 | * @param in | |
299 | * the input data | |
300 | * @param url | |
301 | * the {@link URL} used to locate the cached resource | |
302 | * | |
59654e2a NR |
303 | * @return the number of bytes written |
304 | * | |
8816d2f7 NR |
305 | * @throws IOException |
306 | * in case of I/O error | |
307 | */ | |
59654e2a | 308 | public long save(InputStream in, URL url) throws IOException { |
8816d2f7 | 309 | File cached = getCached(url); |
59654e2a | 310 | return save(in, cached); |
8816d2f7 NR |
311 | } |
312 | ||
313 | /** | |
314 | * Save the given resource to the cache. | |
f6e8d60d NR |
315 | * <p> |
316 | * Will also clean the {@link Cache} from old files. | |
8816d2f7 NR |
317 | * |
318 | * @param in | |
319 | * the input data | |
320 | * @param cached | |
321 | * the cached {@link File} to save to | |
322 | * | |
59654e2a NR |
323 | * @return the number of bytes written |
324 | * | |
8816d2f7 NR |
325 | * @throws IOException |
326 | * in case of I/O error | |
327 | */ | |
59654e2a | 328 | private long save(InputStream in, File cached) throws IOException { |
05c4f401 | 329 | // We delete AFTER so not to remove the subdir we will use... |
59654e2a | 330 | long bytes = IOUtils.write(in, cached); |
05c4f401 | 331 | clean(true, dir, 10); |
59654e2a | 332 | return bytes; |
8816d2f7 NR |
333 | } |
334 | ||
2ee6c205 NR |
335 | /** |
336 | * Remove the given resource from the cache. | |
337 | * | |
338 | * @param uniqueID | |
339 | * a unique ID used to locate the cached resource | |
340 | * | |
341 | * @return TRUE if it was removed | |
342 | */ | |
343 | public boolean remove(String uniqueID) { | |
344 | File cached = getCached(uniqueID); | |
345 | return cached.delete(); | |
346 | } | |
347 | ||
348 | /** | |
349 | * Remove the given resource from the cache. | |
350 | * | |
351 | * @param url | |
352 | * the {@link URL} used to locate the cached resource | |
353 | * | |
354 | * @return TRUE if it was removed | |
355 | */ | |
356 | public boolean remove(URL url) { | |
357 | File cached = getCached(url); | |
358 | return cached.delete(); | |
359 | } | |
360 | ||
8816d2f7 NR |
361 | /** |
362 | * Check if the {@link File} is too old according to | |
363 | * {@link Cache#tooOldChanging}. | |
364 | * | |
365 | * @param file | |
366 | * the file to check | |
367 | * @param stable | |
368 | * TRUE to denote stable files, that are not supposed to change | |
369 | * too often | |
370 | * | |
371 | * @return TRUE if it is | |
372 | */ | |
373 | private boolean isOld(File file, boolean stable) { | |
374 | long max = tooOldChanging; | |
375 | if (stable) { | |
376 | max = tooOldStable; | |
377 | } | |
378 | ||
379 | if (max < 0) { | |
380 | return false; | |
381 | } | |
382 | ||
383 | long time = new Date().getTime() - file.lastModified(); | |
384 | if (time < 0) { | |
530d4062 NR |
385 | tracer.error("Timestamp in the future for file: " |
386 | + file.getAbsolutePath()); | |
8816d2f7 NR |
387 | } |
388 | ||
389 | return time < 0 || time > max; | |
390 | } | |
391 | ||
392 | /** | |
393 | * Return the associated cache {@link File} from this {@link URL}. | |
394 | * | |
395 | * @param url | |
396 | * the {@link URL} | |
397 | * | |
398 | * @return the cached {@link File} version of this {@link URL} | |
399 | */ | |
400 | private File getCached(URL url) { | |
401 | File subdir; | |
402 | ||
403 | String name = url.getHost(); | |
404 | if (name == null || name.isEmpty()) { | |
405 | // File | |
406 | File file = new File(url.getFile()); | |
d827da2a NR |
407 | if (file.getParent() == null) { |
408 | subdir = new File("+"); | |
409 | } else { | |
410 | subdir = new File(file.getParent().replace("..", "__")); | |
411 | } | |
8816d2f7 NR |
412 | subdir = new File(dir, allowedChars(subdir.getPath())); |
413 | name = allowedChars(url.getFile()); | |
414 | } else { | |
415 | // URL | |
416 | File subsubDir = new File(dir, allowedChars(url.getHost())); | |
417 | subdir = new File(subsubDir, "_" + allowedChars(url.getPath())); | |
418 | name = allowedChars("_" + url.getQuery()); | |
419 | } | |
420 | ||
421 | File cacheFile = new File(subdir, name); | |
422 | subdir.mkdirs(); | |
423 | ||
424 | return cacheFile; | |
425 | } | |
426 | ||
427 | /** | |
428 | * Get the basic cache resource file corresponding to this unique ID. | |
429 | * <p> | |
430 | * Note that you may need to add a sub-directory in some cases. | |
431 | * | |
432 | * @param uniqueID | |
433 | * the id | |
434 | * | |
435 | * @return the cached version if present, NULL if not | |
436 | */ | |
437 | private File getCached(String uniqueID) { | |
438 | File file = new File(dir, allowedChars(uniqueID)); | |
439 | File subdir = new File(file.getParentFile(), "_"); | |
440 | return new File(subdir, file.getName()); | |
441 | } | |
442 | ||
443 | /** | |
444 | * Replace not allowed chars (in a {@link File}) by "_". | |
445 | * | |
446 | * @param raw | |
447 | * the raw {@link String} | |
448 | * | |
449 | * @return the sanitised {@link String} | |
450 | */ | |
451 | private String allowedChars(String raw) { | |
452 | return raw.replace('/', '_').replace(':', '_').replace("\\", "_"); | |
453 | } | |
454 | } |