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}