Move default tmp dirs, fix BLANK handling
[fanfix.git] / src / be / nikiroo / fanfix / Cache.java
CommitLineData
08fe2e33
NR
1package be.nikiroo.fanfix;
2
3import java.io.BufferedOutputStream;
4import java.io.File;
5import java.io.FileInputStream;
6import java.io.FileOutputStream;
7import java.io.IOException;
8import java.io.InputStream;
9import java.net.CookieHandler;
10import java.net.CookieManager;
11import java.net.CookiePolicy;
12import java.net.CookieStore;
13import java.net.HttpCookie;
14import java.net.HttpURLConnection;
15import java.net.URISyntaxException;
16import java.net.URL;
17import java.net.URLConnection;
08fe2e33
NR
18import java.util.Date;
19import java.util.Map;
20import java.util.zip.GZIPInputStream;
21
22import javax.imageio.ImageIO;
23
24import be.nikiroo.fanfix.bundles.Config;
25import be.nikiroo.fanfix.supported.BasicSupport;
26import be.nikiroo.utils.IOUtils;
27import be.nikiroo.utils.MarkableFileInputStream;
08fe2e33
NR
28
29/**
30 * This cache will manage Internet (and local) downloads, as well as put the
31 * downloaded files into a cache.
32 * <p>
33 * As long the cached resource is not too old, it will use it instead of
34 * retrieving the file again.
35 *
36 * @author niki
37 */
38public class Cache {
39 private File dir;
40 private String UA;
41 private long tooOldChanging;
42 private long tooOldStable;
43 private CookieManager cookies;
44
45 /**
46 * Create a new {@link Cache} object.
47 *
48 * @param dir
49 * the directory to use as cache
50 * @param UA
51 * the User-Agent to use to download the resources
52 * @param hoursChanging
53 * the number of hours after which a cached file that is thought
54 * to change ~often is considered too old (or -1 for
55 * "never too old")
56 * @param hoursStable
57 * the number of hours after which a LARGE cached file that is
58 * thought to change rarely is considered too old (or -1 for
59 * "never too old")
60 *
61 * @throws IOException
62 * in case of I/O error
63 */
64 public Cache(File dir, String UA, int hoursChanging, int hoursStable)
65 throws IOException {
66 this.dir = dir;
67 this.UA = UA;
68 this.tooOldChanging = 1000 * 60 * 60 * hoursChanging;
69 this.tooOldStable = 1000 * 60 * 60 * hoursStable;
70
71 if (dir != null) {
72 if (!dir.exists()) {
73 dir.mkdirs();
74 }
75 }
76
77 if (dir == null || !dir.exists()) {
78 throw new IOException("Cannot create the cache directory: "
79 + (dir == null ? "null" : dir.getAbsolutePath()));
80 }
81
82 cookies = new CookieManager();
83 cookies.setCookiePolicy(CookiePolicy.ACCEPT_ALL);
84 CookieHandler.setDefault(cookies);
85 }
86
87 /**
88 * Open a resource (will load it from the cache if possible, or save it into
89 * the cache after downloading if not).
90 *
91 * @param url
92 * the resource to open
93 * @param support
94 * the support to use to download the resource
95 * @param stable
96 * TRUE for more stable resources, FALSE when they often change
97 *
595dfa7a 98 * @return the opened resource, NOT NULL
08fe2e33
NR
99 *
100 * @throws IOException
101 * in case of I/O error
102 */
103 public InputStream open(URL url, BasicSupport support, boolean stable)
104 throws IOException {
595dfa7a 105 // MUST NOT return null
08fe2e33
NR
106 return open(url, support, stable, url);
107 }
108
109 /**
110 * Open a resource (will load it from the cache if possible, or save it into
111 * the cache after downloading if not).
112 * <p>
113 * The cached resource will be assimilated to the given original {@link URL}
114 *
115 * @param url
116 * the resource to open
117 * @param support
118 * the support to use to download the resource
119 * @param stable
120 * TRUE for more stable resources, FALSE when they often change
121 * @param originalUrl
122 * the original {@link URL} used to locate the cached resource
123 *
595dfa7a 124 * @return the opened resource, NOT NULL
08fe2e33
NR
125 *
126 * @throws IOException
127 * in case of I/O error
128 */
129 public InputStream open(URL url, BasicSupport support, boolean stable,
130 URL originalUrl) throws IOException {
595dfa7a 131 // MUST NOT return null
08fe2e33
NR
132 try {
133 InputStream in = load(originalUrl, false, stable);
134 if (in == null) {
135 try {
136 save(url, support, originalUrl);
137 } catch (IOException e) {
138 throw new IOException("Cannot save the url: "
139 + (url == null ? "null" : url.toString()), e);
140 }
141
595dfa7a 142 // Was just saved, can load old, so, will not be null
08fe2e33
NR
143 in = load(originalUrl, true, stable);
144 }
145
146 return in;
147 } catch (IOException e) {
148 throw new IOException("Cannot open the url: "
149 + (url == null ? "null" : url.toString()), e);
150 }
151 }
152
153 /**
154 * Refresh the resource into cache if needed.
155 *
156 * @param url
157 * the resource to open
158 * @param support
159 * the support to use to download the resource
160 * @param stable
161 * TRUE for more stable resources, FALSE when they often change
162 *
08fe2e33
NR
163 * @throws IOException
164 * in case of I/O error
165 */
166 public void refresh(URL url, BasicSupport support, boolean stable)
167 throws IOException {
168 File cached = getCached(url);
169 if (cached.exists() && !isOld(cached, stable)) {
170 return;
171 }
172
173 open(url, support, stable).close();
174 }
175
176 /**
177 * Check the resource to see if it is in the cache.
178 *
179 * @param url
180 * the resource to check
181 *
182 * @return TRUE if it is
183 *
184 */
185 public boolean check(URL url) {
186 return getCached(url).exists();
187 }
188
189 /**
190 * Open a resource (will load it from the cache if possible, or save it into
191 * the cache after downloading if not) as an Image, then save it where
192 * requested.
193 * <p>
194 * This version will not always work properly if the original file was not
195 * downloaded before.
196 *
197 * @param url
198 * the resource to open
199 *
200 * @return the opened resource image
201 *
202 * @throws IOException
203 * in case of I/O error
204 */
205 public void saveAsImage(URL url, File target) throws IOException {
595dfa7a 206 URL cachedUrl = new URL(url.toString());
08fe2e33
NR
207 File cached = getCached(cachedUrl);
208
209 if (!cached.exists() || isOld(cached, true)) {
595dfa7a
NR
210 InputStream imageIn = open(url, null, true);
211 ImageIO.write(IOUtils.toImage(imageIn), Instance.getConfig()
08fe2e33
NR
212 .getString(Config.IMAGE_FORMAT_CONTENT).toLowerCase(),
213 cached);
214 }
215
216 IOUtils.write(new FileInputStream(cached), target);
217 }
218
219 /**
220 * Manually add this item to the cache.
221 *
222 * @param in
223 * the input data
224 * @param uniqueID
225 * a unique ID for this resource
226 *
9252c65e 227 * @return the resulting {@link File}
08fe2e33
NR
228 *
229 * @throws IOException
230 * in case of I/O error
231 */
232 public File addToCache(InputStream in, String uniqueID) throws IOException {
233 File file = getCached(new File(uniqueID).toURI().toURL());
234 IOUtils.write(in, file);
235 return file;
236 }
237
238 /**
239 * Clean the cache (delete the cached items).
240 *
241 * @param onlyOld
242 * only clean the files that are considered too old
243 *
244 * @return the number of cleaned items
245 */
246 public int cleanCache(boolean onlyOld) {
247 int num = 0;
248 for (File file : dir.listFiles()) {
249 if (!onlyOld || isOld(file, true)) {
250 if (file.delete()) {
251 num++;
252 } else {
253 System.err.println("Cannot delete temporary file: "
254 + file.getAbsolutePath());
255 }
256 }
257 }
258 return num;
259 }
260
261 /**
262 * Open a resource from the cache if it exists.
263 *
264 * @param url
265 * the resource to open
595dfa7a
NR
266 *
267 * @return the opened resource if found, NULL i not
268 *
08fe2e33
NR
269 * @throws IOException
270 * in case of I/O error
271 */
272 private InputStream load(URL url, boolean allowOld, boolean stable)
273 throws IOException {
274 File cached = getCached(url);
275 if (cached.exists() && !isOld(cached, stable)) {
276 return new MarkableFileInputStream(new FileInputStream(cached));
277 }
278
279 return null;
280 }
281
282 /**
283 * Save the given resource to the cache.
284 *
285 * @param url
286 * the resource
287 * @param support
288 * the {@link BasicSupport} used to download it
289 * @param originalUrl
290 * the original {@link URL} used to locate the cached resource
291 *
292 * @throws IOException
293 * in case of I/O error
294 * @throws URISyntaxException
295 */
296 private void save(URL url, BasicSupport support, URL originalUrl)
297 throws IOException {
298 URLConnection conn = url.openConnection();
299
300 conn.setRequestProperty("User-Agent", UA);
301 conn.setRequestProperty("Cookie", generateCookies(support));
302 conn.setRequestProperty("Accept-Encoding", "gzip");
89cb07a6 303 if (support != null && support.getCurrentReferer() != null) {
08fe2e33
NR
304 conn.setRequestProperty("Referer", support.getCurrentReferer()
305 .toString());
306 conn.setRequestProperty("Host", support.getCurrentReferer()
307 .getHost());
308 }
309
310 conn.connect();
311
312 // Check if redirect
313 if (conn instanceof HttpURLConnection
314 && ((HttpURLConnection) conn).getResponseCode() / 100 == 3) {
315 String newUrl = conn.getHeaderField("Location");
316 save(new URL(newUrl), support, originalUrl);
317 return;
318 }
319
320 InputStream in = conn.getInputStream();
321 if ("gzip".equals(conn.getContentEncoding())) {
322 in = new GZIPInputStream(in);
323 }
324
325 try {
326 File cached = getCached(originalUrl);
327 BufferedOutputStream out = new BufferedOutputStream(
328 new FileOutputStream(cached));
329 try {
330 byte[] buf = new byte[4096];
331 int len;
332 while ((len = in.read(buf)) > 0) {
333 out.write(buf, 0, len);
334 }
335 } finally {
336 out.close();
337 }
338 } finally {
339 in.close();
340 }
341 }
342
343 /**
344 * Check if the {@link File} is too old according to
345 * {@link Cache#tooOldChanging}.
346 *
347 * @param file
348 * the file to check
349 * @param stable
350 * TRUE to denote files that are not supposed to change too often
351 *
352 * @return TRUE if it is
353 */
354 private boolean isOld(File file, boolean stable) {
355 long max = tooOldChanging;
356 if (stable) {
357 max = tooOldStable;
358 }
359
360 if (max < 0) {
361 return false;
362 }
363
364 long time = new Date().getTime() - file.lastModified();
365 if (time < 0) {
366 System.err.println("Timestamp in the future for file: "
367 + file.getAbsolutePath());
368 }
369
370 return time < 0 || time > max;
371 }
372
373 /**
374 * Get the cache resource from the cache if it is present for this
375 * {@link URL}.
376 *
377 * @param url
378 * the url
379 * @return the cached version if present, NULL if not
380 */
381 private File getCached(URL url) {
382 String name = url.getHost();
383 if (name == null || name.length() == 0) {
384 name = url.getFile();
385 } else {
386 name = url.toString();
387 }
388
389 name = name.replace('/', '_').replace(':', '_');
390
391 return new File(dir, name);
392 }
393
394 /**
395 * Generate the cookie {@link String} from the local {@link CookieStore} so
396 * it is ready to be passed.
397 *
398 * @return the cookie
399 */
400 private String generateCookies(BasicSupport support) {
401 StringBuilder builder = new StringBuilder();
402 for (HttpCookie cookie : cookies.getCookieStore().getCookies()) {
403 if (builder.length() > 0) {
404 builder.append(';');
405 }
406
407 // TODO: check if format is ok
408 builder.append(cookie.toString());
409 }
410
411 if (support != null) {
412 for (Map.Entry<String, String> set : support.getCookies()
413 .entrySet()) {
414 if (builder.length() > 0) {
415 builder.append(';');
416 }
417 builder.append(set.getKey());
418 builder.append('=');
419 builder.append(set.getValue());
420 }
421 }
422
423 return builder.toString();
424 }
425}