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