Fix Cache (URL to File could fail if no parent)
[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
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
NR
23
24 /**
25 * Create a new {@link Cache} object.
26 *
27 * @param dir
28 * the directory to use as cache
29 * @param hoursChanging
30 * the number of hours after which a cached file that is thought
31 * to change ~often is considered too old (or -1 for
32 * "never too old")
33 * @param hoursStable
34 * the number of hours after which a cached file that is thought
35 * to change rarely is considered too old (or -1 for
36 * "never too old")
37 *
38 * @throws IOException
39 * in case of I/O error
40 */
41 public Cache(File dir, int hoursChanging, int hoursStable)
42 throws IOException {
43 this.dir = dir;
44 this.tooOldChanging = 1000 * 60 * 60 * hoursChanging;
45 this.tooOldStable = 1000 * 60 * 60 * hoursStable;
46
47 if (dir != null && !dir.exists()) {
48 dir.mkdirs();
49 }
50
51 if (dir == null || !dir.exists()) {
52 throw new IOException("Cannot create the cache directory: "
53 + (dir == null ? "null" : dir.getAbsolutePath()));
54 }
55 }
56
530d4062
NR
57 /**
58 * The traces handler for this {@link Cache}.
59 *
60 * @return the traces handler
61 */
62 public TraceHandler getTraceHandler() {
63 return tracer;
64 }
65
66 /**
67 * The traces handler for this {@link Cache}.
68 *
69 * @param tracer
70 * the new traces handler
71 */
72 public void setTraceHandler(TraceHandler tracer) {
73 this.tracer = tracer;
74 }
75
8816d2f7
NR
76 /**
77 * Check the resource to see if it is in the cache.
78 *
79 * @param url
80 * the resource to check
81 * @param allowTooOld
82 * allow files even if they are considered too old
83 * @param stable
84 * a stable file (that dones't change too often) -- parameter
85 * used to check if the file is too old to keep or not
86 *
87 * @return TRUE if it is
88 *
89 */
90 public boolean check(URL url, boolean allowTooOld, boolean stable) {
91 File file = getCached(url);
92 if (file.exists()) {
93 if (allowTooOld || !isOld(file, stable)) {
94 return true;
95 }
96 }
97
98 return false;
99 }
100
101 /**
102 * Clean the cache (delete the cached items).
103 *
104 * @param onlyOld
105 * only clean the files that are considered too old for a stable
106 * resource
107 *
108 * @return the number of cleaned items
109 */
110 public int clean(boolean onlyOld) {
111 return clean(onlyOld, dir);
112 }
113
8816d2f7
NR
114 /**
115 * Clean the cache (delete the cached items) in the given cache directory.
116 *
117 * @param onlyOld
118 * only clean the files that are considered too old for stable
119 * resources
120 * @param cacheDir
121 * the cache directory to clean
122 *
123 * @return the number of cleaned items
124 */
125 private int clean(boolean onlyOld, File cacheDir) {
126 int num = 0;
127 for (File file : cacheDir.listFiles()) {
128 if (file.isDirectory()) {
129 num += clean(onlyOld, file);
130 } else {
131 if (!onlyOld || isOld(file, true)) {
132 if (file.delete()) {
133 num++;
134 } else {
530d4062
NR
135 tracer.error("Cannot delete temporary file: "
136 + file.getAbsolutePath());
8816d2f7
NR
137 }
138 }
139 }
140 }
141
142 return num;
143 }
144
145 /**
146 * Open a resource from the cache if it exists.
147 *
148 * @param uniqueID
149 * the unique ID
150 * @param allowTooOld
151 * allow files even if they are considered too old
152 * @param stable
153 * a stable file (that dones't change too often) -- parameter
154 * used to check if the file is too old to keep or not
155 *
156 * @return the opened resource if found, NULL if not
8816d2f7
NR
157 */
158 public InputStream load(String uniqueID, boolean allowTooOld, boolean stable) {
159 return load(getCached(uniqueID), allowTooOld, stable);
160 }
161
162 /**
163 * Open a resource from the cache if it exists.
164 *
165 * @param url
166 * the resource to open
167 * @param allowTooOld
168 * allow files even if they are considered too old
169 * @param stable
170 * a stable file (that dones't change too often) -- parameter
171 * used to check if the file is too old to keep or not
172 *
173 * @return the opened resource if found, NULL if not
8816d2f7 174 */
2ee6c205 175 public InputStream load(URL url, boolean allowTooOld, boolean stable) {
8816d2f7
NR
176 return load(getCached(url), allowTooOld, stable);
177 }
178
179 /**
180 * Open a resource from the cache if it exists.
181 *
2ee6c205 182 * @param cached
8816d2f7
NR
183 * the resource to open
184 * @param allowTooOld
185 * allow files even if they are considered too old
186 * @param stable
187 * a stable file (that dones't change too often) -- parameter
188 * used to check if the file is too old to keep or not
189 *
190 * @return the opened resource if found, NULL if not
8816d2f7
NR
191 */
192 private InputStream load(File cached, boolean allowTooOld, boolean stable) {
193 if (cached.exists() && (allowTooOld || !isOld(cached, stable))) {
194 try {
195 return new MarkableFileInputStream(new FileInputStream(cached));
196 } catch (FileNotFoundException e) {
197 return null;
198 }
199 }
200
201 return null;
202 }
203
204 /**
205 * Save the given resource to the cache.
206 *
207 * @param in
208 * the input data
209 * @param uniqueID
210 * a unique ID used to locate the cached resource
211 *
212 * @return the resulting {@link File}
213 *
214 * @throws IOException
215 * in case of I/O error
216 */
217 public File save(InputStream in, String uniqueID) throws IOException {
218 File cached = getCached(uniqueID);
219 cached.getParentFile().mkdirs();
220 return save(in, cached);
221 }
222
223 /**
224 * Save the given resource to the cache.
225 *
226 * @param in
227 * the input data
228 * @param url
229 * the {@link URL} used to locate the cached resource
230 *
2ee6c205
NR
231 * @return the actual cache file
232 *
8816d2f7
NR
233 * @throws IOException
234 * in case of I/O error
235 */
236 public File save(InputStream in, URL url) throws IOException {
237 File cached = getCached(url);
238 return save(in, cached);
239 }
240
241 /**
242 * Save the given resource to the cache.
243 *
244 * @param in
245 * the input data
246 * @param cached
247 * the cached {@link File} to save to
248 *
2ee6c205
NR
249 * @return the actual cache file
250 *
8816d2f7
NR
251 * @throws IOException
252 * in case of I/O error
253 */
254 private File save(InputStream in, File cached) throws IOException {
255 IOUtils.write(in, cached);
256 return cached;
257 }
258
2ee6c205
NR
259 /**
260 * Remove the given resource from the cache.
261 *
262 * @param uniqueID
263 * a unique ID used to locate the cached resource
264 *
265 * @return TRUE if it was removed
266 */
267 public boolean remove(String uniqueID) {
268 File cached = getCached(uniqueID);
269 return cached.delete();
270 }
271
272 /**
273 * Remove the given resource from the cache.
274 *
275 * @param url
276 * the {@link URL} used to locate the cached resource
277 *
278 * @return TRUE if it was removed
279 */
280 public boolean remove(URL url) {
281 File cached = getCached(url);
282 return cached.delete();
283 }
284
8816d2f7
NR
285 /**
286 * Check if the {@link File} is too old according to
287 * {@link Cache#tooOldChanging}.
288 *
289 * @param file
290 * the file to check
291 * @param stable
292 * TRUE to denote stable files, that are not supposed to change
293 * too often
294 *
295 * @return TRUE if it is
296 */
297 private boolean isOld(File file, boolean stable) {
298 long max = tooOldChanging;
299 if (stable) {
300 max = tooOldStable;
301 }
302
303 if (max < 0) {
304 return false;
305 }
306
307 long time = new Date().getTime() - file.lastModified();
308 if (time < 0) {
530d4062
NR
309 tracer.error("Timestamp in the future for file: "
310 + file.getAbsolutePath());
8816d2f7
NR
311 }
312
313 return time < 0 || time > max;
314 }
315
316 /**
317 * Return the associated cache {@link File} from this {@link URL}.
318 *
319 * @param url
320 * the {@link URL}
321 *
322 * @return the cached {@link File} version of this {@link URL}
323 */
324 private File getCached(URL url) {
325 File subdir;
326
327 String name = url.getHost();
328 if (name == null || name.isEmpty()) {
329 // File
330 File file = new File(url.getFile());
d827da2a
NR
331 if (file.getParent() == null) {
332 subdir = new File("+");
333 } else {
334 subdir = new File(file.getParent().replace("..", "__"));
335 }
8816d2f7
NR
336 subdir = new File(dir, allowedChars(subdir.getPath()));
337 name = allowedChars(url.getFile());
338 } else {
339 // URL
340 File subsubDir = new File(dir, allowedChars(url.getHost()));
341 subdir = new File(subsubDir, "_" + allowedChars(url.getPath()));
342 name = allowedChars("_" + url.getQuery());
343 }
344
345 File cacheFile = new File(subdir, name);
346 subdir.mkdirs();
347
348 return cacheFile;
349 }
350
351 /**
352 * Get the basic cache resource file corresponding to this unique ID.
353 * <p>
354 * Note that you may need to add a sub-directory in some cases.
355 *
356 * @param uniqueID
357 * the id
358 *
359 * @return the cached version if present, NULL if not
360 */
361 private File getCached(String uniqueID) {
362 File file = new File(dir, allowedChars(uniqueID));
363 File subdir = new File(file.getParentFile(), "_");
364 return new File(subdir, file.getName());
365 }
366
367 /**
368 * Replace not allowed chars (in a {@link File}) by "_".
369 *
370 * @param raw
371 * the raw {@link String}
372 *
373 * @return the sanitised {@link String}
374 */
375 private String allowedChars(String raw) {
376 return raw.replace('/', '_').replace(':', '_').replace("\\", "_");
377 }
378}