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