001    // License: GPL. For details, see LICENSE file.
002    package org.openstreetmap.josm.io;
003    
004    import java.io.BufferedInputStream;
005    import java.io.BufferedOutputStream;
006    import java.io.File;
007    import java.io.FileInputStream;
008    import java.io.FileOutputStream;
009    import java.io.IOException;
010    import java.io.UnsupportedEncodingException;
011    import java.util.Date;
012    
013    import org.openstreetmap.josm.Main;
014    
015    /**
016     * Use this class if you want to cache and store a single file that gets updated regularly.
017     * Unless you flush() it will be kept in memory. If you want to cache a lot of data and/or files,
018     * use CacheFiles
019     * @param <T> a {@link Throwable} that may be thrown during {@link #updateData()},
020     * use {@link RuntimeException} if no exception must be handled.
021     * @author xeen
022     *
023     */
024    public abstract class CacheCustomContent<T extends Throwable> {
025        /**
026         * Common intervals
027         */
028        final static public int INTERVAL_ALWAYS = -1;
029        final static public int INTERVAL_HOURLY = 60*60;
030        final static public int INTERVAL_DAILY = INTERVAL_HOURLY * 24;
031        final static public int INTERVAL_WEEKLY = INTERVAL_DAILY * 7;
032        final static public int INTERVAL_MONTHLY = INTERVAL_WEEKLY * 4;
033        final static public int INTERVAL_NEVER = Integer.MAX_VALUE;
034    
035        /**
036         * Where the data will be stored
037         */
038        private byte[] data = null;
039    
040        /**
041         * The ident that identifies the stored file. Includes file-ending.
042         */
043        final private String ident;
044    
045        /**
046         * The (file-)path where the data will be stored
047         */
048        final private File path;
049    
050        /**
051         * How often to update the cached version
052         */
053        final private int updateInterval;
054    
055        /**
056         * This function will be executed when an update is required. It has to be implemented by the
057         * inheriting class and should use a worker if it has a long wall time as the function is
058         * executed in the current thread.
059         * @return the data to cache
060         */
061        protected abstract byte[] updateData() throws T;
062    
063        /**
064         * This function serves as a comfort hook to perform additional checks if the cache is valid
065         * @return True if the cached copy is still valid
066         */
067        protected boolean isCacheValid() {
068            return true;
069        }
070    
071        /**
072         * Initializes the class. Note that all read data will be stored in memory until it is flushed
073         * by flushData().
074         * @param ident
075         * @param updateInterval
076         */
077        public CacheCustomContent(String ident, int updateInterval) {
078            this.ident = ident;
079            this.updateInterval = updateInterval;
080            this.path = new File(Main.pref.getCacheDirectory(), ident);
081        }
082    
083        /**
084         * Updates data if required
085         * @return Returns the data
086         */
087        public byte[] updateIfRequired() throws T {
088            if (Main.pref.getInteger("cache." + ident, 0) + updateInterval < new Date().getTime()/1000
089                    || !isCacheValid())
090                return updateForce();
091            return getData();
092        }
093    
094        /**
095         * Updates data if required
096         * @return Returns the data as string
097         */
098        public String updateIfRequiredString() throws T {
099            if (Main.pref.getInteger("cache." + ident, 0) + updateInterval < new Date().getTime()/1000
100                    || !isCacheValid())
101                return updateForceString();
102            return getDataString();
103        }
104    
105        /**
106         * Executes an update regardless of updateInterval
107         * @return Returns the data
108         */
109        public byte[] updateForce() throws T {
110            this.data = updateData();
111            saveToDisk();
112            Main.pref.putInteger("cache." + ident, (int)(new Date().getTime()/1000));
113            return data;
114        }
115    
116        /**
117         * Executes an update regardless of updateInterval
118         * @return Returns the data as String
119         */
120        public String updateForceString() throws T {
121            updateForce();
122            try {
123                return new String(data, "utf-8");
124            } catch (UnsupportedEncodingException e){
125                e.printStackTrace();
126                return "";
127            }
128        }
129    
130        /**
131         * Returns the data without performing any updates
132         * @return the data
133         */
134        public byte[] getData() throws T {
135            if (data == null) {
136                loadFromDisk();
137            }
138            return data;
139        }
140    
141        /**
142         * Returns the data without performing any updates
143         * @return the data as String
144         */
145        public String getDataString() throws T {
146            try {
147                return new String(getData(), "utf-8");
148            } catch(UnsupportedEncodingException e){
149                e.printStackTrace();
150                return "";
151            }
152        }
153    
154        /**
155         * Tries to load the data using the given ident from disk. If this fails, data will be updated
156         */
157        private void loadFromDisk() throws T {
158            if (Main.applet)
159                this.data = updateForce();
160            else {
161                try {
162                    BufferedInputStream input = new BufferedInputStream(new FileInputStream(path));
163                    this.data = new byte[input.available()];
164                    input.read(this.data);
165                    input.close();
166                } catch (IOException e) {
167                    this.data = updateForce();
168                }
169            }
170        }
171    
172        /**
173         * Stores the data to disk
174         */
175        private void saveToDisk() {
176            if (Main.applet)
177                return;
178            try {
179                BufferedOutputStream output = new BufferedOutputStream(new FileOutputStream(path));
180                output.write(this.data);
181                output.flush();
182                output.close();
183            } catch(Exception e) {
184                e.printStackTrace();
185            }
186        }
187    
188        /**
189         * Flushes the data from memory. Class automatically reloads it from disk or updateData() if
190         * required
191         */
192        public void flushData() {
193            data = null;
194        }
195    }