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