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