001// License: GPL. For details, see LICENSE file.
002package org.openstreetmap.josm.io;
003
004import java.io.BufferedInputStream;
005import java.io.BufferedOutputStream;
006import java.io.File;
007import java.io.FileInputStream;
008import java.io.FileOutputStream;
009import java.io.IOException;
010import java.io.UnsupportedEncodingException;
011
012import org.openstreetmap.josm.Main;
013import org.openstreetmap.josm.tools.Utils;
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 */
024public 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 < System.currentTimeMillis()/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 < System.currentTimeMillis()/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)(System.currentTimeMillis()/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            BufferedInputStream input = null;
162            try {
163                input = new BufferedInputStream(new FileInputStream(path));
164                this.data = new byte[input.available()];
165                input.read(this.data);
166            } catch (IOException e) {
167                this.data = updateForce();
168            } finally {
169                Utils.close(input);
170            }
171        }
172    }
173
174    /**
175     * Stores the data to disk
176     */
177    private void saveToDisk() {
178        if (Main.applet)
179            return;
180        BufferedOutputStream output = null;
181        try {
182            output = new BufferedOutputStream(new FileOutputStream(path));
183            output.write(this.data);
184            output.flush();
185        } catch(Exception e) {
186            e.printStackTrace();
187        } finally {
188            Utils.close(output);
189        }
190    }
191
192    /**
193     * Flushes the data from memory. Class automatically reloads it from disk or updateData() if
194     * required
195     */
196    public void flushData() {
197        data = null;
198    }
199}