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