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            return true;
100        }
101    }
102
103    protected void checkOfflineAccess() {
104        // To be overriden by subclasses
105    }
106
107    /**
108     * Updates data if required
109     * @return Returns the data
110     * @throws T if an error occurs
111     */
112    public byte[] updateIfRequired() throws T {
113        if (needsUpdate())
114            return updateForce();
115        return getData();
116    }
117
118    /**
119     * Updates data if required
120     * @return Returns the data as string
121     * @throws T if an error occurs
122     */
123    public String updateIfRequiredString() throws T {
124        if (needsUpdate())
125            return updateForceString();
126        return getDataString();
127    }
128
129    /**
130     * Executes an update regardless of updateInterval
131     * @return Returns the data
132     * @throws T if an error occurs
133     */
134    public byte[] updateForce() throws T {
135        this.data = updateData();
136        saveToDisk();
137        Main.pref.putInteger("cache." + ident, (int) (System.currentTimeMillis()/1000));
138        return data;
139    }
140
141    /**
142     * Executes an update regardless of updateInterval
143     * @return Returns the data as String
144     * @throws T if an error occurs
145     */
146    public String updateForceString() throws T {
147        updateForce();
148        return new String(data, StandardCharsets.UTF_8);
149    }
150
151    /**
152     * Returns the data without performing any updates
153     * @return the data
154     * @throws T if an error occurs
155     */
156    public byte[] getData() throws T {
157        if (data == null) {
158            loadFromDisk();
159        }
160        return data;
161    }
162
163    /**
164     * Returns the data without performing any updates
165     * @return the data as String
166     * @throws T if an error occurs
167     */
168    public String getDataString() throws T {
169        byte[] array = getData();
170        if (array == null) {
171            return null;
172        }
173        return new String(array, StandardCharsets.UTF_8);
174    }
175
176    /**
177     * Tries to load the data using the given ident from disk. If this fails, data will be updated, unless run in offline mode
178     * @throws T a {@link Throwable}
179     */
180    private void loadFromDisk() throws T {
181        try (BufferedInputStream input = new BufferedInputStream(new FileInputStream(path))) {
182            this.data = new byte[input.available()];
183            if (input.read(this.data) < this.data.length) {
184                Main.error("Failed to read expected contents from "+path);
185            }
186        } catch (IOException e) {
187            if (!isOffline()) {
188                this.data = updateForce();
189            }
190        }
191    }
192
193    /**
194     * Stores the data to disk
195     */
196    private void saveToDisk() {
197        try (BufferedOutputStream output = new BufferedOutputStream(new FileOutputStream(path))) {
198            output.write(this.data);
199            output.flush();
200        } catch (IOException e) {
201            Main.error(e);
202        }
203    }
204
205    /**
206     * Flushes the data from memory. Class automatically reloads it from disk or updateData() if required
207     */
208    public void flushData() {
209        data = null;
210    }
211}