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