001 // License: GPL. For details, see LICENSE file. 002 package org.openstreetmap.josm.io; 003 004 import java.awt.image.BufferedImage; 005 import java.io.File; 006 import java.io.RandomAccessFile; 007 import java.math.BigInteger; 008 import java.security.MessageDigest; 009 import java.util.Date; 010 import java.util.Iterator; 011 import java.util.Set; 012 import java.util.TreeMap; 013 014 import javax.imageio.ImageIO; 015 016 import org.openstreetmap.josm.Main; 017 018 /** 019 * Use this class if you want to cache a lot of files that shouldn't be kept in memory. You can 020 * specify how much data should be stored and after which date the files should be expired. 021 * This works on a last-access basis, so files get deleted after they haven't been used for x days. 022 * You can turn this off by calling setUpdateModTime(false). Files get deleted on a first-in-first-out 023 * basis. 024 * @author xeen 025 * 026 */ 027 public class CacheFiles { 028 /** 029 * Common expirey dates 030 */ 031 final static public int EXPIRE_NEVER = -1; 032 final static public int EXPIRE_DAILY = 60 * 60 * 24; 033 final static public int EXPIRE_WEEKLY = EXPIRE_DAILY * 7; 034 final static public int EXPIRE_MONTHLY = EXPIRE_WEEKLY * 4; 035 036 final private File dir; 037 final private String ident; 038 final private boolean enabled; 039 040 private long expire; // in seconds 041 private long maxsize; // in megabytes 042 private boolean updateModTime = true; 043 044 // If the cache is full, we don't want to delete just one file 045 private static final int CLEANUP_TRESHOLD = 20; 046 // We don't want to clean after every file-write 047 private static final int CLEANUP_INTERVAL = 5; 048 // Stores how many files have been written 049 private int writes = 0; 050 051 /** 052 * Creates a new cache class. The ident will be used to store the files on disk and to save 053 * expire/space settings. 054 * @param ident 055 */ 056 public CacheFiles(String ident) { 057 this(ident, true); 058 } 059 060 public CacheFiles(String ident, boolean isPlugin) { 061 String pref = isPlugin ? 062 Main.pref.getPluginsDirectory().getPath() + File.separator + "cache" : 063 Main.pref.getCacheDirectory().getPath(); 064 065 boolean dir_writeable; 066 this.ident = ident; 067 String cacheDir = Main.pref.get("cache." + ident + "." + "path", pref + File.separator + ident + File.separator); 068 this.dir = new File(cacheDir); 069 try { 070 this.dir.mkdirs(); 071 dir_writeable = true; 072 } catch(Exception e) { 073 // We have no access to this directory, so don't do anything 074 dir_writeable = false; 075 } 076 this.enabled = dir_writeable; 077 this.expire = Main.pref.getLong("cache." + ident + "." + "expire", EXPIRE_DAILY); 078 if(this.expire < 0) { 079 this.expire = CacheFiles.EXPIRE_NEVER; 080 } 081 this.maxsize = Main.pref.getLong("cache." + ident + "." + "maxsize", 50); 082 if(this.maxsize < 0) { 083 this.maxsize = -1; 084 } 085 } 086 087 /** 088 * Loads the data for the given ident as an byte array. Returns null if data not available. 089 * @param ident 090 * @return 091 */ 092 public byte[] getData(String ident) { 093 if(!enabled) return null; 094 try { 095 File data = getPath(ident); 096 if(!data.exists()) 097 return null; 098 099 if(isExpired(data)) { 100 data.delete(); 101 return null; 102 } 103 104 // Update last mod time so we don't expire recently used data 105 if(updateModTime) { 106 data.setLastModified(new Date().getTime()); 107 } 108 109 byte[] bytes = new byte[(int) data.length()]; 110 new RandomAccessFile(data, "r").readFully(bytes); 111 return bytes; 112 } catch(Exception e) { 113 System.out.println(e.getMessage()); 114 } 115 return null; 116 } 117 118 /** 119 * Writes an byte-array to disk 120 * @param ident 121 * @param data 122 */ 123 public void saveData(String ident, byte[] data) { 124 if(!enabled) return; 125 try { 126 File f = getPath(ident); 127 if(f.exists()) { 128 f.delete(); 129 } 130 // rws also updates the file meta-data, i.e. last mod time 131 new RandomAccessFile(f, "rws").write(data); 132 } catch(Exception e){ 133 System.out.println(e.getMessage()); 134 } 135 136 writes++; 137 checkCleanUp(); 138 } 139 140 /** 141 * Loads the data for the given ident as an image. If no image is found, null is returned 142 * @param ident Identifier 143 * @return BufferedImage or null 144 */ 145 public BufferedImage getImg(String ident) { 146 if(!enabled) return null; 147 try { 148 File img = getPath(ident, "png"); 149 if(!img.exists()) 150 return null; 151 152 if(isExpired(img)) { 153 img.delete(); 154 return null; 155 } 156 // Update last mod time so we don't expire recently used images 157 if(updateModTime) { 158 img.setLastModified(new Date().getTime()); 159 } 160 return ImageIO.read(img); 161 } catch(Exception e) { 162 System.out.println(e.getMessage()); 163 } 164 return null; 165 } 166 167 /** 168 * Saves a given image and ident to the cache 169 * @param ident 170 * @param image 171 */ 172 public void saveImg(String ident, BufferedImage image) { 173 if(!enabled) return; 174 try { 175 ImageIO.write(image, "png", getPath(ident, "png")); 176 } catch(Exception e){ 177 System.out.println(e.getMessage()); 178 } 179 180 writes++; 181 checkCleanUp(); 182 } 183 184 /** 185 * Sets the amount of time data is stored before it gets expired 186 * @param amount of time in seconds 187 * @param force will also write it to the preferences 188 */ 189 public void setExpire(int amount, boolean force) { 190 String key = "cache." + ident + "." + "expire"; 191 if(!Main.pref.get(key).isEmpty() && !force) 192 return; 193 194 this.expire = amount > 0 ? amount : EXPIRE_NEVER; 195 Main.pref.putLong(key, this.expire); 196 } 197 198 /** 199 * Sets the amount of data stored in the cache 200 * @param amount in Megabytes 201 * @param force will also write it to the preferences 202 */ 203 public void setMaxSize(int amount, boolean force) { 204 String key = "cache." + ident + "." + "maxsize"; 205 if(!Main.pref.get(key).isEmpty() && !force) 206 return; 207 208 this.maxsize = amount > 0 ? amount : -1; 209 Main.pref.putLong(key, this.maxsize); 210 } 211 212 /** 213 * Call this with true to update the last modification time when a file it is read. 214 * Call this with false to not update the last modification time when a file is read. 215 * @param to 216 */ 217 public void setUpdateModTime(boolean to) { 218 updateModTime = to; 219 } 220 221 /** 222 * Checks if a clean up is needed and will do so if necessary 223 */ 224 public void checkCleanUp() { 225 if(this.writes > CLEANUP_INTERVAL) { 226 cleanUp(); 227 } 228 } 229 230 /** 231 * Performs a default clean up with the set values (deletes oldest files first) 232 */ 233 public void cleanUp() { 234 if(!this.enabled || maxsize == -1) return; 235 236 TreeMap<Long, File> modtime = new TreeMap<Long, File>(); 237 long dirsize = 0; 238 239 for(File f : dir.listFiles()) { 240 if(isExpired(f)) { 241 f.delete(); 242 } else { 243 dirsize += f.length(); 244 modtime.put(f.lastModified(), f); 245 } 246 } 247 248 if(dirsize < maxsize*1000*1000) return; 249 250 Set<Long> keySet = modtime.keySet(); 251 Iterator<Long> it = keySet.iterator(); 252 int i=0; 253 while (it.hasNext()) { 254 i++; 255 modtime.get(it.next()).delete(); 256 257 // Delete a couple of files, then check again 258 if(i % CLEANUP_TRESHOLD == 0 && getDirSize() < maxsize) 259 return; 260 } 261 writes = 0; 262 } 263 264 final static public int CLEAN_ALL = 0; 265 final static public int CLEAN_SMALL_FILES = 1; 266 final static public int CLEAN_BY_DATE = 2; 267 /** 268 * Performs a non-default, specified clean up 269 * @param type any of the CLEAN_XX constants. 270 * @param size for CLEAN_SMALL_FILES: deletes all files smaller than (size) bytes 271 */ 272 public void customCleanUp(int type, int size) { 273 switch(type) { 274 case CLEAN_ALL: 275 for(File f : dir.listFiles()) { 276 f.delete(); 277 } 278 break; 279 case CLEAN_SMALL_FILES: 280 for(File f: dir.listFiles()) 281 if(f.length() < size) { 282 f.delete(); 283 } 284 break; 285 case CLEAN_BY_DATE: 286 cleanUp(); 287 break; 288 } 289 } 290 291 /** 292 * Calculates the size of the directory 293 * @return long Size of directory in bytes 294 */ 295 private long getDirSize() { 296 if(!enabled) return -1; 297 long dirsize = 0; 298 299 for(File f : this.dir.listFiles()) { 300 dirsize += f.length(); 301 } 302 return dirsize; 303 } 304 305 /** 306 * Returns a short and unique file name for a given long identifier 307 * @return String short filename 308 */ 309 private static String getUniqueFilename(String ident) { 310 try { 311 MessageDigest md = MessageDigest.getInstance("MD5"); 312 BigInteger number = new BigInteger(1, md.digest(ident.getBytes())); 313 return number.toString(16); 314 } catch(Exception e) { 315 // Fall back. Remove unsuitable characters and some random ones to shrink down path length. 316 // Limit it to 70 characters, that leaves about 190 for the path on Windows/NTFS 317 ident = ident.replaceAll("[^a-zA-Z0-9]", ""); 318 ident = ident.replaceAll("[acegikmoqsuwy]", ""); 319 return ident.substring(ident.length() - 70); 320 } 321 } 322 323 /** 324 * Gets file path for ident with customizable file-ending 325 * @param ident 326 * @param ending 327 * @return File 328 */ 329 private File getPath(String ident, String ending) { 330 return new File(dir, getUniqueFilename(ident) + "." + ending); 331 } 332 333 /** 334 * Gets file path for ident 335 * @param ident 336 * @param ending 337 * @return File 338 */ 339 private File getPath(String ident) { 340 return new File(dir, getUniqueFilename(ident)); 341 } 342 343 /** 344 * Checks whether a given file is expired 345 * @param file 346 * @return expired? 347 */ 348 private boolean isExpired(File file) { 349 if(CacheFiles.EXPIRE_NEVER == this.expire) 350 return false; 351 return (file.lastModified() < (new Date().getTime() - expire*1000)); 352 } 353 }