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