code cleanup, fix for ReplaceInputStream
[nikiroo-utils.git] / src / be / nikiroo / utils / Cache.java
CommitLineData
8816d2f7
NR
1package be.nikiroo.utils;
2
3import java.io.File;
8816d2f7
NR
4import java.io.FileNotFoundException;
5import java.io.IOException;
6import java.io.InputStream;
7import java.net.URL;
8import java.util.Date;
9
8e76f6ab
NR
10import 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 */
19public 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 *
8816d2f7
NR
284 * @throws IOException
285 * in case of I/O error
286 */
e8aa5bf9 287 public void save(InputStream in, String uniqueID) throws IOException {
8816d2f7
NR
288 File cached = getCached(uniqueID);
289 cached.getParentFile().mkdirs();
e8aa5bf9 290 save(in, cached);
8816d2f7
NR
291 }
292
293 /**
294 * Save the given resource to the cache.
295 *
296 * @param in
297 * the input data
298 * @param url
299 * the {@link URL} used to locate the cached resource
300 *
301 * @throws IOException
302 * in case of I/O error
303 */
e8aa5bf9 304 public void save(InputStream in, URL url) throws IOException {
8816d2f7 305 File cached = getCached(url);
e8aa5bf9 306 save(in, cached);
8816d2f7
NR
307 }
308
309 /**
310 * Save the given resource to the cache.
f6e8d60d
NR
311 * <p>
312 * Will also clean the {@link Cache} from old files.
8816d2f7
NR
313 *
314 * @param in
315 * the input data
316 * @param cached
317 * the cached {@link File} to save to
318 *
319 * @throws IOException
320 * in case of I/O error
321 */
e8aa5bf9 322 private void save(InputStream in, File cached) throws IOException {
05c4f401 323 // We delete AFTER so not to remove the subdir we will use...
8816d2f7 324 IOUtils.write(in, cached);
05c4f401 325 clean(true, dir, 10);
8816d2f7
NR
326 }
327
2ee6c205
NR
328 /**
329 * Remove the given resource from the cache.
330 *
331 * @param uniqueID
332 * a unique ID used to locate the cached resource
333 *
334 * @return TRUE if it was removed
335 */
336 public boolean remove(String uniqueID) {
337 File cached = getCached(uniqueID);
338 return cached.delete();
339 }
340
341 /**
342 * Remove the given resource from the cache.
343 *
344 * @param url
345 * the {@link URL} used to locate the cached resource
346 *
347 * @return TRUE if it was removed
348 */
349 public boolean remove(URL url) {
350 File cached = getCached(url);
351 return cached.delete();
352 }
353
8816d2f7
NR
354 /**
355 * Check if the {@link File} is too old according to
356 * {@link Cache#tooOldChanging}.
357 *
358 * @param file
359 * the file to check
360 * @param stable
361 * TRUE to denote stable files, that are not supposed to change
362 * too often
363 *
364 * @return TRUE if it is
365 */
366 private boolean isOld(File file, boolean stable) {
367 long max = tooOldChanging;
368 if (stable) {
369 max = tooOldStable;
370 }
371
372 if (max < 0) {
373 return false;
374 }
375
376 long time = new Date().getTime() - file.lastModified();
377 if (time < 0) {
530d4062
NR
378 tracer.error("Timestamp in the future for file: "
379 + file.getAbsolutePath());
8816d2f7
NR
380 }
381
382 return time < 0 || time > max;
383 }
384
385 /**
386 * Return the associated cache {@link File} from this {@link URL}.
387 *
388 * @param url
389 * the {@link URL}
390 *
391 * @return the cached {@link File} version of this {@link URL}
392 */
393 private File getCached(URL url) {
394 File subdir;
395
396 String name = url.getHost();
397 if (name == null || name.isEmpty()) {
398 // File
399 File file = new File(url.getFile());
d827da2a
NR
400 if (file.getParent() == null) {
401 subdir = new File("+");
402 } else {
403 subdir = new File(file.getParent().replace("..", "__"));
404 }
8816d2f7
NR
405 subdir = new File(dir, allowedChars(subdir.getPath()));
406 name = allowedChars(url.getFile());
407 } else {
408 // URL
409 File subsubDir = new File(dir, allowedChars(url.getHost()));
410 subdir = new File(subsubDir, "_" + allowedChars(url.getPath()));
411 name = allowedChars("_" + url.getQuery());
412 }
413
414 File cacheFile = new File(subdir, name);
415 subdir.mkdirs();
416
417 return cacheFile;
418 }
419
420 /**
421 * Get the basic cache resource file corresponding to this unique ID.
422 * <p>
423 * Note that you may need to add a sub-directory in some cases.
424 *
425 * @param uniqueID
426 * the id
427 *
428 * @return the cached version if present, NULL if not
429 */
430 private File getCached(String uniqueID) {
431 File file = new File(dir, allowedChars(uniqueID));
432 File subdir = new File(file.getParentFile(), "_");
433 return new File(subdir, file.getName());
434 }
435
436 /**
437 * Replace not allowed chars (in a {@link File}) by "_".
438 *
439 * @param raw
440 * the raw {@link String}
441 *
442 * @return the sanitised {@link String}
443 */
444 private String allowedChars(String raw) {
445 return raw.replace('/', '_').replace(':', '_').replace("\\", "_");
446 }
447}