18b811e397f09c241af4ebaef49a2a1662c4c8a0
[nikiroo-utils.git] / src / be / nikiroo / utils / Cache.java
1 package be.nikiroo.utils;
2
3 import java.io.File;
4 import java.io.FileInputStream;
5 import java.io.FileNotFoundException;
6 import java.io.IOException;
7 import java.io.InputStream;
8 import java.net.URL;
9 import 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 */
18 public class Cache {
19 private File dir;
20 private long tooOldChanging;
21 private long tooOldStable;
22 private TraceHandler tracer = new TraceHandler();
23
24 /**
25 * Only for inheritance.
26 */
27 protected Cache() {
28 }
29
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;
50 this.tooOldChanging = 1000L * 60 * 60 * hoursChanging;
51 this.tooOldStable = 1000L * 60 * 60 * hoursStable;
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
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) {
79 if (tracer == null) {
80 tracer = new TraceHandler(false, false, false);
81 }
82
83 this.tracer = tracer;
84 }
85
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
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) {
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()) {
138 if (!allowTooOld && isOld(cached, stable)) {
139 if (!cached.delete()) {
140 tracer.error("Cannot delete temporary file: "
141 + cached.getAbsolutePath());
142 }
143 } else {
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) {
161 return clean(onlyOld, dir);
162 }
163
164 /**
165 * Clean the cache (delete the cached items) in the given cache directory.
166 *
167 * @param onlyOld
168 * only clean the files that are considered too old for stable
169 * resources
170 * @param cacheDir
171 * the cache directory to clean
172 *
173 * @return the number of cleaned items
174 */
175 private int clean(boolean onlyOld, File cacheDir) {
176 long ms = System.currentTimeMillis();
177
178 tracer.trace("Cleaning cache from old files...");
179
180 int num = doClean(onlyOld, cacheDir);
181
182 tracer.trace("Cache cleaned in " + (System.currentTimeMillis() - ms)
183 + " ms");
184
185 return num;
186 }
187
188 /**
189 * Actual work done for {@link Cache#clean(boolean, File)}.
190 *
191 * @param onlyOld
192 * only clean the files that are considered too old for stable
193 * resources
194 * @param cacheDir
195 * the cache directory to clean
196 *
197 * @return the number of cleaned items
198 */
199 private int doClean(boolean onlyOld, File cacheDir) {
200 int num = 0;
201 File[] files = cacheDir.listFiles();
202 if (files != null) {
203 for (File file : files) {
204 if (file.isDirectory()) {
205 num += doClean(onlyOld, file);
206 } else {
207 if (!onlyOld || isOld(file, true)) {
208 if (file.delete()) {
209 num++;
210 } else {
211 tracer.error("Cannot delete temporary file: "
212 + file.getAbsolutePath());
213 }
214 }
215 }
216 }
217 }
218
219 return num;
220 }
221
222 /**
223 * Open a resource from the cache if it exists.
224 *
225 * @param uniqueID
226 * the unique ID
227 * @param allowTooOld
228 * allow files even if they are considered too old
229 * @param stable
230 * a stable file (that dones't change too often) -- parameter
231 * used to check if the file is too old to keep or not
232 *
233 * @return the opened resource if found, NULL if not
234 */
235 public InputStream load(String uniqueID, boolean allowTooOld, boolean stable) {
236 return load(getCached(uniqueID), allowTooOld, stable);
237 }
238
239 /**
240 * Open a resource from the cache if it exists.
241 *
242 * @param url
243 * the resource to open
244 * @param allowTooOld
245 * allow files even if they are considered too old
246 * @param stable
247 * a stable file (that doesn't change too often) -- parameter
248 * used to check if the file is too old to keep or not in the
249 * cache
250 *
251 * @return the opened resource if found, NULL if not
252 */
253 public InputStream load(URL url, boolean allowTooOld, boolean stable) {
254 return load(getCached(url), allowTooOld, stable);
255 }
256
257 /**
258 * Open a resource from the cache if it exists.
259 *
260 * @param cached
261 * the resource to open
262 * @param allowTooOld
263 * allow files even if they are considered too old
264 * @param stable
265 * a stable file (that dones't change too often) -- parameter
266 * used to check if the file is too old to keep or not
267 *
268 * @return the opened resource if found, NULL if not
269 */
270 private InputStream load(File cached, boolean allowTooOld, boolean stable) {
271 if (cached.exists() && cached.isFile()
272 && (allowTooOld || !isOld(cached, stable))) {
273 try {
274 return new MarkableFileInputStream(new FileInputStream(cached));
275 } catch (FileNotFoundException e) {
276 return null;
277 }
278 }
279
280 return null;
281 }
282
283 /**
284 * Save the given resource to the cache.
285 *
286 * @param in
287 * the input data
288 * @param uniqueID
289 * a unique ID used to locate the cached resource
290 *
291 * @throws IOException
292 * in case of I/O error
293 */
294 public void save(InputStream in, String uniqueID) throws IOException {
295 File cached = getCached(uniqueID);
296 cached.getParentFile().mkdirs();
297 save(in, cached);
298 }
299
300 /**
301 * Save the given resource to the cache.
302 *
303 * @param in
304 * the input data
305 * @param url
306 * the {@link URL} used to locate the cached resource
307 *
308 * @throws IOException
309 * in case of I/O error
310 */
311 public void save(InputStream in, URL url) throws IOException {
312 File cached = getCached(url);
313 save(in, cached);
314 }
315
316 /**
317 * Save the given resource to the cache.
318 * <p>
319 * Will also clean the {@link Cache} from old files.
320 *
321 * @param in
322 * the input data
323 * @param cached
324 * the cached {@link File} to save to
325 *
326 * @throws IOException
327 * in case of I/O error
328 */
329 private void save(InputStream in, File cached) throws IOException {
330 clean(true);
331 IOUtils.write(in, cached);
332 }
333
334 /**
335 * Remove the given resource from the cache.
336 *
337 * @param uniqueID
338 * a unique ID used to locate the cached resource
339 *
340 * @return TRUE if it was removed
341 */
342 public boolean remove(String uniqueID) {
343 File cached = getCached(uniqueID);
344 return cached.delete();
345 }
346
347 /**
348 * Remove the given resource from the cache.
349 *
350 * @param url
351 * the {@link URL} used to locate the cached resource
352 *
353 * @return TRUE if it was removed
354 */
355 public boolean remove(URL url) {
356 File cached = getCached(url);
357 return cached.delete();
358 }
359
360 /**
361 * Check if the {@link File} is too old according to
362 * {@link Cache#tooOldChanging}.
363 *
364 * @param file
365 * the file to check
366 * @param stable
367 * TRUE to denote stable files, that are not supposed to change
368 * too often
369 *
370 * @return TRUE if it is
371 */
372 private boolean isOld(File file, boolean stable) {
373 long max = tooOldChanging;
374 if (stable) {
375 max = tooOldStable;
376 }
377
378 if (max < 0) {
379 return false;
380 }
381
382 long time = new Date().getTime() - file.lastModified();
383 if (time < 0) {
384 tracer.error("Timestamp in the future for file: "
385 + file.getAbsolutePath());
386 }
387
388 return time < 0 || time > max;
389 }
390
391 /**
392 * Return the associated cache {@link File} from this {@link URL}.
393 *
394 * @param url
395 * the {@link URL}
396 *
397 * @return the cached {@link File} version of this {@link URL}
398 */
399 private File getCached(URL url) {
400 File subdir;
401
402 String name = url.getHost();
403 if (name == null || name.isEmpty()) {
404 // File
405 File file = new File(url.getFile());
406 if (file.getParent() == null) {
407 subdir = new File("+");
408 } else {
409 subdir = new File(file.getParent().replace("..", "__"));
410 }
411 subdir = new File(dir, allowedChars(subdir.getPath()));
412 name = allowedChars(url.getFile());
413 } else {
414 // URL
415 File subsubDir = new File(dir, allowedChars(url.getHost()));
416 subdir = new File(subsubDir, "_" + allowedChars(url.getPath()));
417 name = allowedChars("_" + url.getQuery());
418 }
419
420 File cacheFile = new File(subdir, name);
421 subdir.mkdirs();
422
423 return cacheFile;
424 }
425
426 /**
427 * Get the basic cache resource file corresponding to this unique ID.
428 * <p>
429 * Note that you may need to add a sub-directory in some cases.
430 *
431 * @param uniqueID
432 * the id
433 *
434 * @return the cached version if present, NULL if not
435 */
436 private File getCached(String uniqueID) {
437 File file = new File(dir, allowedChars(uniqueID));
438 File subdir = new File(file.getParentFile(), "_");
439 return new File(subdir, file.getName());
440 }
441
442 /**
443 * Replace not allowed chars (in a {@link File}) by "_".
444 *
445 * @param raw
446 * the raw {@link String}
447 *
448 * @return the sanitised {@link String}
449 */
450 private String allowedChars(String raw) {
451 return raw.replace('/', '_').replace(':', '_').replace("\\", "_");
452 }
453 }