source: josm/trunk/src/org/openstreetmap/josm/io/CacheFiles.java@ 8282

Last change on this file since 8282 was 7132, checked in by Don-vip, 10 years ago

fix #9984 - Add support for WMS tiles defining a transparent color in RGB space (tRNS PNG chunk for example), instead of a proper alpha channel. Surprisingly, Java does not support that out of the box, ImageIO.read always returns opaque images. Allows to switch between this mode and standard mode using WMS layer contextual entry "Use Alpha Channel", for consistency with images defining an alpha channel. Does not impact other images than WMS tiles.

  • Property svn:eol-style set to native
File size: 11.2 KB
Line 
1// License: GPL. For details, see LICENSE file.
2package org.openstreetmap.josm.io;
3
4import java.awt.image.BufferedImage;
5import java.io.File;
6import java.io.RandomAccessFile;
7import java.math.BigInteger;
8import java.nio.charset.StandardCharsets;
9import java.security.MessageDigest;
10import java.util.Iterator;
11import java.util.Set;
12import java.util.TreeMap;
13
14import javax.imageio.ImageIO;
15
16import org.openstreetmap.josm.Main;
17import org.openstreetmap.josm.tools.ImageProvider;
18
19/**
20 * Use this class if you want to cache a lot of files that shouldn't be kept in memory. You can
21 * specify how much data should be stored and after which date the files should be expired.
22 * This works on a last-access basis, so files get deleted after they haven't been used for x days.
23 * You can turn this off by calling setUpdateModTime(false). Files get deleted on a first-in-first-out
24 * basis.
25 * @author xeen
26 *
27 */
28public class CacheFiles {
29 /**
30 * Common expirey dates
31 */
32 public static final int EXPIRE_NEVER = -1;
33 public static final int EXPIRE_DAILY = 60 * 60 * 24;
34 public static final int EXPIRE_WEEKLY = EXPIRE_DAILY * 7;
35 public static final int EXPIRE_MONTHLY = EXPIRE_WEEKLY * 4;
36
37 private final File dir;
38 private final String ident;
39 private final boolean enabled;
40
41 private long expire; // in seconds
42 private long maxsize; // in megabytes
43 private boolean updateModTime = true;
44
45 // If the cache is full, we don't want to delete just one file
46 private static final int CLEANUP_TRESHOLD = 20;
47 // We don't want to clean after every file-write
48 private static final int CLEANUP_INTERVAL = 5;
49 // Stores how many files have been written
50 private int writes = 0;
51
52 /**
53 * Creates a new cache class. The ident will be used to store the files on disk and to save
54 * expire/space settings. Set plugin state to <code>true</code>.
55 * @param ident cache identifier
56 */
57 public CacheFiles(String ident) {
58 this(ident, true);
59 }
60
61 /**
62 * Creates a new cache class. The ident will be used to store the files on disk and to save
63 * expire/space settings.
64 * @param ident cache identifier
65 * @param isPlugin Whether this is a plugin or not (changes cache path)
66 */
67 public CacheFiles(String ident, boolean isPlugin) {
68 String pref = isPlugin ?
69 Main.pref.getPluginsDirectory().getPath() + File.separator + "cache" :
70 Main.pref.getCacheDirectory().getPath();
71
72 boolean dir_writeable;
73 this.ident = ident;
74 String cacheDir = Main.pref.get("cache." + ident + "." + "path", pref + File.separator + ident + File.separator);
75 this.dir = new File(cacheDir);
76 try {
77 this.dir.mkdirs();
78 dir_writeable = true;
79 } catch(Exception e) {
80 // We have no access to this directory, so don't do anything
81 dir_writeable = false;
82 }
83 this.enabled = dir_writeable;
84 this.expire = Main.pref.getLong("cache." + ident + "." + "expire", EXPIRE_DAILY);
85 if(this.expire < 0) {
86 this.expire = CacheFiles.EXPIRE_NEVER;
87 }
88 this.maxsize = Main.pref.getLong("cache." + ident + "." + "maxsize", 50);
89 if(this.maxsize < 0) {
90 this.maxsize = -1;
91 }
92 }
93
94 /**
95 * Loads the data for the given ident as an byte array. Returns null if data not available.
96 * @param ident cache identifier
97 * @return stored data
98 */
99 public byte[] getData(String ident) {
100 if(!enabled) return null;
101 try {
102 File data = getPath(ident);
103 if(!data.exists())
104 return null;
105
106 if(isExpired(data)) {
107 data.delete();
108 return null;
109 }
110
111 // Update last mod time so we don't expire recently used data
112 if(updateModTime) {
113 data.setLastModified(System.currentTimeMillis());
114 }
115
116 byte[] bytes = new byte[(int) data.length()];
117 try (RandomAccessFile raf = new RandomAccessFile(data, "r")) {
118 raf.readFully(bytes);
119 }
120 return bytes;
121 } catch (Exception e) {
122 Main.warn(e);
123 }
124 return null;
125 }
126
127 /**
128 * Writes an byte-array to disk
129 * @param ident cache identifier
130 * @param data data to store
131 */
132 public void saveData(String ident, byte[] data) {
133 if(!enabled) return;
134 try {
135 File f = getPath(ident);
136 if (f.exists()) {
137 f.delete();
138 }
139 // rws also updates the file meta-data, i.e. last mod time
140 try (RandomAccessFile raf = new RandomAccessFile(f, "rws")) {
141 raf.write(data);
142 }
143 } catch (Exception e) {
144 Main.warn(e);
145 }
146
147 writes++;
148 checkCleanUp();
149 }
150
151 /**
152 * Loads the data for the given ident as an image. If no image is found, null is returned
153 * @param ident cache identifier
154 * @return BufferedImage or null
155 */
156 public BufferedImage getImg(String ident) {
157 if(!enabled) return null;
158 try {
159 File img = getPath(ident, "png");
160 if(!img.exists())
161 return null;
162
163 if(isExpired(img)) {
164 img.delete();
165 return null;
166 }
167 // Update last mod time so we don't expire recently used images
168 if(updateModTime) {
169 img.setLastModified(System.currentTimeMillis());
170 }
171 return ImageProvider.read(img, false, false);
172 } catch (Exception e) {
173 Main.warn(e);
174 }
175 return null;
176 }
177
178 /**
179 * Saves a given image and ident to the cache
180 * @param ident cache identifier
181 * @param image imaga data for storage
182 */
183 public void saveImg(String ident, BufferedImage image) {
184 if (!enabled) return;
185 try {
186 ImageIO.write(image, "png", getPath(ident, "png"));
187 } catch (Exception e) {
188 Main.warn(e);
189 }
190
191 writes++;
192 checkCleanUp();
193 }
194
195 /**
196 * Sets the amount of time data is stored before it gets expired
197 * @param amount of time in seconds
198 * @param force will also write it to the preferences
199 */
200 public void setExpire(int amount, boolean force) {
201 String key = "cache." + ident + "." + "expire";
202 if(!Main.pref.get(key).isEmpty() && !force)
203 return;
204
205 this.expire = amount > 0 ? amount : EXPIRE_NEVER;
206 Main.pref.putLong(key, this.expire);
207 }
208
209 /**
210 * Sets the amount of data stored in the cache
211 * @param amount in Megabytes
212 * @param force will also write it to the preferences
213 */
214 public void setMaxSize(int amount, boolean force) {
215 String key = "cache." + ident + "." + "maxsize";
216 if(!Main.pref.get(key).isEmpty() && !force)
217 return;
218
219 this.maxsize = amount > 0 ? amount : -1;
220 Main.pref.putLong(key, this.maxsize);
221 }
222
223 /**
224 * Call this with <code>true</code> to update the last modification time when a file it is read.
225 * Call this with <code>false</code> to not update the last modification time when a file is read.
226 * @param to update state
227 */
228 public void setUpdateModTime(boolean to) {
229 updateModTime = to;
230 }
231
232 /**
233 * Checks if a clean up is needed and will do so if necessary
234 */
235 public void checkCleanUp() {
236 if(this.writes > CLEANUP_INTERVAL) {
237 cleanUp();
238 }
239 }
240
241 /**
242 * Performs a default clean up with the set values (deletes oldest files first)
243 */
244 public void cleanUp() {
245 if(!this.enabled || maxsize == -1) return;
246
247 TreeMap<Long, File> modtime = new TreeMap<>();
248 long dirsize = 0;
249
250 for(File f : dir.listFiles()) {
251 if(isExpired(f)) {
252 f.delete();
253 } else {
254 dirsize += f.length();
255 modtime.put(f.lastModified(), f);
256 }
257 }
258
259 if(dirsize < maxsize*1000*1000) return;
260
261 Set<Long> keySet = modtime.keySet();
262 Iterator<Long> it = keySet.iterator();
263 int i=0;
264 while (it.hasNext()) {
265 i++;
266 modtime.get(it.next()).delete();
267
268 // Delete a couple of files, then check again
269 if(i % CLEANUP_TRESHOLD == 0 && getDirSize() < maxsize)
270 return;
271 }
272 writes = 0;
273 }
274
275 public static final int CLEAN_ALL = 0;
276 public static final int CLEAN_SMALL_FILES = 1;
277 public static final int CLEAN_BY_DATE = 2;
278
279 /**
280 * Performs a non-default, specified clean up
281 * @param type any of the CLEAN_XX constants.
282 * @param size for CLEAN_SMALL_FILES: deletes all files smaller than (size) bytes
283 */
284 public void customCleanUp(int type, int size) {
285 switch(type) {
286 case CLEAN_ALL:
287 for(File f : dir.listFiles()) {
288 f.delete();
289 }
290 break;
291 case CLEAN_SMALL_FILES:
292 for(File f: dir.listFiles())
293 if(f.length() < size) {
294 f.delete();
295 }
296 break;
297 case CLEAN_BY_DATE:
298 cleanUp();
299 break;
300 }
301 }
302
303 /**
304 * Calculates the size of the directory
305 * @return long Size of directory in bytes
306 */
307 private long getDirSize() {
308 if(!enabled) return -1;
309 long dirsize = 0;
310
311 for(File f : this.dir.listFiles()) {
312 dirsize += f.length();
313 }
314 return dirsize;
315 }
316
317 /**
318 * Returns a short and unique file name for a given long identifier
319 * @return String short filename
320 */
321 private static String getUniqueFilename(String ident) {
322 try {
323 MessageDigest md = MessageDigest.getInstance("MD5");
324 BigInteger number = new BigInteger(1, md.digest(ident.getBytes(StandardCharsets.UTF_8)));
325 return number.toString(16);
326 } catch(Exception e) {
327 // Fall back. Remove unsuitable characters and some random ones to shrink down path length.
328 // Limit it to 70 characters, that leaves about 190 for the path on Windows/NTFS
329 ident = ident.replaceAll("[^a-zA-Z0-9]", "");
330 ident = ident.replaceAll("[acegikmoqsuwy]", "");
331 return ident.substring(ident.length() - 70);
332 }
333 }
334
335 /**
336 * Gets file path for ident with customizable file-ending
337 * @param ident cache identifier
338 * @param ending file extension
339 * @return file structure
340 */
341 private File getPath(String ident, String ending) {
342 return new File(dir, getUniqueFilename(ident) + "." + ending);
343 }
344
345 /**
346 * Gets file path for ident
347 * @param ident cache identifier
348 * @return file structure
349 */
350 private File getPath(String ident) {
351 return new File(dir, getUniqueFilename(ident));
352 }
353
354 /**
355 * Checks whether a given file is expired
356 * @param file file description structure
357 * @return expired state
358 */
359 private boolean isExpired(File file) {
360 if(CacheFiles.EXPIRE_NEVER == this.expire)
361 return false;
362 return (file.lastModified() < (System.currentTimeMillis() - expire*1000));
363 }
364}
Note: See TracBrowser for help on using the repository browser.