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    }