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