001// License: GPL. For details, see LICENSE file. 002package org.openstreetmap.josm.io; 003 004import java.awt.image.BufferedImage; 005import java.io.File; 006import java.io.RandomAccessFile; 007import java.math.BigInteger; 008import java.security.MessageDigest; 009import java.util.Iterator; 010import java.util.Set; 011import java.util.TreeMap; 012 013import javax.imageio.ImageIO; 014 015import org.openstreetmap.josm.Main; 016import org.openstreetmap.josm.tools.Utils; 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 */ 027public 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. Set plugin state to <code>true</code>. 054 * @param ident cache identifier 055 */ 056 public CacheFiles(String ident) { 057 this(ident, true); 058 } 059 060 /** 061 * Creates a new cache class. The ident will be used to store the files on disk and to save 062 * expire/space settings. 063 * @param ident cache identifier 064 * @param isPlugin Whether this is a plugin or not (changes cache path) 065 */ 066 public CacheFiles(String ident, boolean isPlugin) { 067 String pref = isPlugin ? 068 Main.pref.getPluginsDirectory().getPath() + File.separator + "cache" : 069 Main.pref.getCacheDirectory().getPath(); 070 071 boolean dir_writeable; 072 this.ident = ident; 073 String cacheDir = Main.pref.get("cache." + ident + "." + "path", pref + File.separator + ident + File.separator); 074 this.dir = new File(cacheDir); 075 try { 076 this.dir.mkdirs(); 077 dir_writeable = true; 078 } catch(Exception e) { 079 // We have no access to this directory, so don't do anything 080 dir_writeable = false; 081 } 082 this.enabled = dir_writeable; 083 this.expire = Main.pref.getLong("cache." + ident + "." + "expire", EXPIRE_DAILY); 084 if(this.expire < 0) { 085 this.expire = CacheFiles.EXPIRE_NEVER; 086 } 087 this.maxsize = Main.pref.getLong("cache." + ident + "." + "maxsize", 50); 088 if(this.maxsize < 0) { 089 this.maxsize = -1; 090 } 091 } 092 093 /** 094 * Loads the data for the given ident as an byte array. Returns null if data not available. 095 * @param ident cache identifier 096 * @return stored data 097 */ 098 public byte[] getData(String ident) { 099 if(!enabled) return null; 100 try { 101 File data = getPath(ident); 102 if(!data.exists()) 103 return null; 104 105 if(isExpired(data)) { 106 data.delete(); 107 return null; 108 } 109 110 // Update last mod time so we don't expire recently used data 111 if(updateModTime) { 112 data.setLastModified(System.currentTimeMillis()); 113 } 114 115 byte[] bytes = new byte[(int) data.length()]; 116 new RandomAccessFile(data, "r").readFully(bytes); 117 return bytes; 118 } catch (Exception e) { 119 Main.warn(e); 120 } 121 return null; 122 } 123 124 /** 125 * Writes an byte-array to disk 126 * @param ident cache identifier 127 * @param data data to store 128 */ 129 public void saveData(String ident, byte[] data) { 130 if(!enabled) return; 131 try { 132 File f = getPath(ident); 133 if (f.exists()) { 134 f.delete(); 135 } 136 // rws also updates the file meta-data, i.e. last mod time 137 RandomAccessFile raf = new RandomAccessFile(f, "rws"); 138 try { 139 raf.write(data); 140 } finally { 141 Utils.close(raf); 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 ImageIO.read(img); 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<Long, File>(); 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 final static public int CLEAN_ALL = 0; 276 final static public int CLEAN_SMALL_FILES = 1; 277 final static public int CLEAN_BY_DATE = 2; 278 /** 279 * Performs a non-default, specified clean up 280 * @param type any of the CLEAN_XX constants. 281 * @param size for CLEAN_SMALL_FILES: deletes all files smaller than (size) bytes 282 */ 283 public void customCleanUp(int type, int size) { 284 switch(type) { 285 case CLEAN_ALL: 286 for(File f : dir.listFiles()) { 287 f.delete(); 288 } 289 break; 290 case CLEAN_SMALL_FILES: 291 for(File f: dir.listFiles()) 292 if(f.length() < size) { 293 f.delete(); 294 } 295 break; 296 case CLEAN_BY_DATE: 297 cleanUp(); 298 break; 299 } 300 } 301 302 /** 303 * Calculates the size of the directory 304 * @return long Size of directory in bytes 305 */ 306 private long getDirSize() { 307 if(!enabled) return -1; 308 long dirsize = 0; 309 310 for(File f : this.dir.listFiles()) { 311 dirsize += f.length(); 312 } 313 return dirsize; 314 } 315 316 /** 317 * Returns a short and unique file name for a given long identifier 318 * @return String short filename 319 */ 320 private static String getUniqueFilename(String ident) { 321 try { 322 MessageDigest md = MessageDigest.getInstance("MD5"); 323 BigInteger number = new BigInteger(1, md.digest(ident.getBytes())); 324 return number.toString(16); 325 } catch(Exception e) { 326 // Fall back. Remove unsuitable characters and some random ones to shrink down path length. 327 // Limit it to 70 characters, that leaves about 190 for the path on Windows/NTFS 328 ident = ident.replaceAll("[^a-zA-Z0-9]", ""); 329 ident = ident.replaceAll("[acegikmoqsuwy]", ""); 330 return ident.substring(ident.length() - 70); 331 } 332 } 333 334 /** 335 * Gets file path for ident with customizable file-ending 336 * @param ident cache identifier 337 * @param ending file extension 338 * @return file structure 339 */ 340 private File getPath(String ident, String ending) { 341 return new File(dir, getUniqueFilename(ident) + "." + ending); 342 } 343 344 /** 345 * Gets file path for ident 346 * @param ident cache identifier 347 * @return file structure 348 */ 349 private File getPath(String ident) { 350 return new File(dir, getUniqueFilename(ident)); 351 } 352 353 /** 354 * Checks whether a given file is expired 355 * @param file file description structure 356 * @return expired state 357 */ 358 private boolean isExpired(File file) { 359 if(CacheFiles.EXPIRE_NEVER == this.expire) 360 return false; 361 return (file.lastModified() < (System.currentTimeMillis() - expire*1000)); 362 } 363}