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