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