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