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;
011import java.util.concurrent.TimeUnit;
012
013import 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, use CacheFiles.
018 * @author xeen
019 * @param <T> a {@link Throwable} that may be thrown during {@link #updateData()},
020 * use {@link RuntimeException} if no exception must be handled.
021 * @since 1450
022 */
023public abstract class CacheCustomContent<T extends Throwable> {
024
025    /** Update interval meaning an update is always needed */
026    public static final int INTERVAL_ALWAYS = -1;
027    /** Update interval meaning an update is needed each hour */
028    public static final int INTERVAL_HOURLY = (int) TimeUnit.HOURS.toSeconds(1);
029    /** Update interval meaning an update is needed each day */
030    public static final int INTERVAL_DAILY = (int) TimeUnit.DAYS.toSeconds(1);
031    /** Update interval meaning an update is needed each week */
032    public static final int INTERVAL_WEEKLY = (int) TimeUnit.DAYS.toSeconds(7);
033    /** Update interval meaning an update is needed each month */
034    public static final int INTERVAL_MONTHLY = (int) TimeUnit.DAYS.toSeconds(28);
035    /** Update interval meaning an update is never needed */
036    public static final int INTERVAL_NEVER = Integer.MAX_VALUE;
037
038    /**
039     * Where the data will be stored
040     */
041    private byte[] data;
042
043    /**
044     * The ident that identifies the stored file. Includes file-ending.
045     */
046    private final String ident;
047
048    /**
049     * The (file-)path where the data will be stored
050     */
051    private final File path;
052
053    /**
054     * How often to update the cached version
055     */
056    private final int updateInterval;
057
058    /**
059     * This function will be executed when an update is required. It has to be implemented by the
060     * inheriting class and should use a worker if it has a long wall time as the function is
061     * executed in the current thread.
062     * @return the data to cache
063     * @throws T a {@link Throwable}
064     */
065    protected abstract byte[] updateData() throws T;
066
067    /**
068     * Initializes the class. Note that all read data will be stored in memory until it is flushed
069     * by flushData().
070     * @param ident ident that identifies the stored file. Includes file-ending.
071     * @param updateInterval update interval in seconds. -1 means always
072     */
073    public CacheCustomContent(String ident, int updateInterval) {
074        this.ident = ident;
075        this.updateInterval = updateInterval;
076        this.path = new File(Main.pref.getCacheDirectory(), ident);
077    }
078
079    /**
080     * This function serves as a comfort hook to perform additional checks if the cache is valid
081     * @return True if the cached copy is still valid
082     */
083    protected boolean isCacheValid() {
084        return true;
085    }
086
087    private boolean needsUpdate() {
088        if (isOffline()) {
089            return false;
090        }
091        return Main.pref.getInteger("cache." + ident, 0) + updateInterval < TimeUnit.MILLISECONDS.toSeconds(System.currentTimeMillis())
092                || !isCacheValid();
093    }
094
095    private boolean isOffline() {
096        try {
097            checkOfflineAccess();
098            return false;
099        } catch (OfflineAccessException e) {
100            Main.trace(e);
101            return true;
102        }
103    }
104
105    protected void checkOfflineAccess() {
106        // To be overriden by subclasses
107    }
108
109    /**
110     * Updates data if required
111     * @return Returns the data
112     * @throws T if an error occurs
113     */
114    public byte[] updateIfRequired() throws T {
115        if (needsUpdate())
116            return updateForce();
117        return getData();
118    }
119
120    /**
121     * Updates data if required
122     * @return Returns the data as string
123     * @throws T if an error occurs
124     */
125    public String updateIfRequiredString() throws T {
126        if (needsUpdate())
127            return updateForceString();
128        return getDataString();
129    }
130
131    /**
132     * Executes an update regardless of updateInterval
133     * @return Returns the data
134     * @throws T if an error occurs
135     */
136    public byte[] updateForce() throws T {
137        this.data = updateData();
138        saveToDisk();
139        Main.pref.putInteger("cache." + ident, (int) (TimeUnit.MILLISECONDS.toSeconds(System.currentTimeMillis())));
140        return data;
141    }
142
143    /**
144     * Executes an update regardless of updateInterval
145     * @return Returns the data as String
146     * @throws T if an error occurs
147     */
148    public String updateForceString() throws T {
149        updateForce();
150        return new String(data, StandardCharsets.UTF_8);
151    }
152
153    /**
154     * Returns the data without performing any updates
155     * @return the data
156     * @throws T if an error occurs
157     */
158    public byte[] getData() throws T {
159        if (data == null) {
160            loadFromDisk();
161        }
162        return data;
163    }
164
165    /**
166     * Returns the data without performing any updates
167     * @return the data as String
168     * @throws T if an error occurs
169     */
170    public String getDataString() throws T {
171        byte[] array = getData();
172        if (array == null) {
173            return null;
174        }
175        return new String(array, StandardCharsets.UTF_8);
176    }
177
178    /**
179     * Tries to load the data using the given ident from disk. If this fails, data will be updated, unless run in offline mode
180     * @throws T a {@link Throwable}
181     */
182    private void loadFromDisk() throws T {
183        try (BufferedInputStream input = new BufferedInputStream(new FileInputStream(path))) {
184            this.data = new byte[input.available()];
185            if (input.read(this.data) < this.data.length) {
186                Main.error("Failed to read expected contents from "+path);
187            }
188        } catch (IOException e) {
189            Main.trace(e);
190            if (!isOffline()) {
191                this.data = updateForce();
192            }
193        }
194    }
195
196    /**
197     * Stores the data to disk
198     */
199    private void saveToDisk() {
200        try (BufferedOutputStream output = new BufferedOutputStream(new FileOutputStream(path))) {
201            output.write(this.data);
202            output.flush();
203        } catch (IOException e) {
204            Main.error(e);
205        }
206    }
207
208    /**
209     * Flushes the data from memory. Class automatically reloads it from disk or updateData() if required
210     */
211    public void flushData() {
212        data = null;
213    }
214}