]> git.basschouten.com Git - openhab-addons.git/blob
adbda0bb7b985e9f75be366bccf70235b6f65e72
[openhab-addons.git] /
1 /**
2  * Copyright (c) 2010-2020 Contributors to the openHAB project
3  *
4  * See the NOTICE file(s) distributed with this work for additional
5  * information.
6  *
7  * This program and the accompanying materials are made available under the
8  * terms of the Eclipse Public License 2.0 which is available at
9  * http://www.eclipse.org/legal/epl-2.0
10  *
11  * SPDX-License-Identifier: EPL-2.0
12  */
13 package org.openhab.binding.darksky.internal.utils;
14
15 import java.io.File;
16 import java.io.IOException;
17 import java.math.BigInteger;
18 import java.nio.charset.StandardCharsets;
19 import java.nio.file.Files;
20 import java.security.MessageDigest;
21 import java.security.NoSuchAlgorithmException;
22 import java.util.Arrays;
23 import java.util.Map;
24 import java.util.concurrent.ConcurrentHashMap;
25 import java.util.concurrent.TimeUnit;
26
27 import org.eclipse.jdt.annotation.NonNullByDefault;
28 import org.eclipse.jdt.annotation.Nullable;
29 import org.openhab.core.config.core.ConfigConstants;
30 import org.slf4j.Logger;
31 import org.slf4j.LoggerFactory;
32
33 /**
34  * This is a simple file based cache implementation.
35  *
36  * @author Christoph Weitkamp - Initial contribution
37  */
38 @NonNullByDefault
39 public class ByteArrayFileCache {
40
41     private final Logger logger = LoggerFactory.getLogger(ByteArrayFileCache.class);
42
43     private static final String MD5_ALGORITHM = "MD5";
44
45     static final String CACHE_FOLDER_NAME = "cache";
46     private static final char EXTENSION_SEPARATOR = '.';
47     private static final char UNIX_SEPARATOR = '/';
48     private static final char WINDOWS_SEPARATOR = '\\';
49
50     private final File cacheFolder;
51
52     static final long ONE_DAY_IN_MILLIS = TimeUnit.DAYS.toMillis(1);
53     private int expiry = 0;
54
55     private static final Map<String, File> FILES_IN_CACHE = new ConcurrentHashMap<>();
56
57     /**
58      * Creates a new {@link ByteArrayFileCache} instance for a service. Creates a <code>cache</code> folder under
59      * <code>$userdata/cache/$servicePID</code>.
60      *
61      * @param servicePID PID of the service
62      */
63     public ByteArrayFileCache(String servicePID) {
64         // TODO track and limit folder size
65         // TODO support user specific folder
66         cacheFolder = new File(new File(ConfigConstants.getUserDataFolder(), CACHE_FOLDER_NAME), servicePID);
67         if (!cacheFolder.exists()) {
68             logger.debug("Creating cache folder '{}'", cacheFolder.getAbsolutePath());
69             cacheFolder.mkdirs();
70         }
71         logger.debug("Using cache folder '{}'", cacheFolder.getAbsolutePath());
72     }
73
74     /**
75      * Creates a new {@link ByteArrayFileCache} instance for a service. Creates a <code>cache</code> folder under
76      * <code>$userdata/cache/$servicePID/</code>.
77      *
78      * @param servicePID PID of the service
79      * @param int the days for how long the files stay in the cache valid. Must be positive. 0 to
80      *            disables this functionality.
81      */
82     public ByteArrayFileCache(String servicePID, int expiry) {
83         this(servicePID);
84         if (expiry < 0) {
85             throw new IllegalArgumentException("Cache expiration time must be greater than or equal to 0");
86         }
87         this.expiry = expiry;
88     }
89
90     /**
91      * Adds a file to the cache. If the cache previously contained a file for the key, the old file is replaced by the
92      * new content.
93      *
94      * @param key the key with which the file is to be associated
95      * @param content the content for the file to be associated with the specified key
96      */
97     public void put(String key, byte[] content) {
98         writeFile(getUniqueFile(key), content);
99     }
100
101     /**
102      * Adds a file to the cache.
103      *
104      * @param key the key with which the file is to be associated
105      * @param content the content for the file to be associated with the specified key
106      */
107     public void putIfAbsent(String key, byte[] content) {
108         File fileInCache = getUniqueFile(key);
109         if (fileInCache.exists()) {
110             logger.debug("File '{}' present in cache", fileInCache.getName());
111             // update time of last use
112             fileInCache.setLastModified(System.currentTimeMillis());
113         } else {
114             writeFile(fileInCache, content);
115         }
116     }
117
118     /**
119      * Adds a file to the cache and returns the content of the file.
120      *
121      * @param key the key with which the file is to be associated
122      * @param content the content for the file to be associated with the specified key
123      * @return the content of the file associated with the given key
124      */
125     public byte[] putIfAbsentAndGet(String key, byte[] content) {
126         putIfAbsent(key, content);
127
128         return content;
129     }
130
131     /**
132      * Writes the given content to the given {@link File}.
133      *
134      * @param fileInCache the {@link File}
135      * @param content the content to be written
136      */
137     private void writeFile(File fileInCache, byte[] content) {
138         logger.debug("Caching file '{}'", fileInCache.getName());
139         try {
140             Files.write(fileInCache.toPath(), content);
141         } catch (IOException e) {
142             logger.warn("Could not write file '{}' to cache", fileInCache.getName(), e);
143         }
144     }
145
146     /**
147      * Checks if the key is present in the cache.
148      *
149      * @param key the key whose presence in the cache is to be tested
150      * @return true if the cache contains a file for the specified key
151      */
152     public boolean containsKey(String key) {
153         return getUniqueFile(key).exists();
154     }
155
156     /**
157      * Removes the file associated with the given key from the cache.
158      *
159      * @param key the key whose associated file is to be removed
160      */
161     public void remove(String key) {
162         deleteFile(getUniqueFile(key));
163     }
164
165     /**
166      * Deletes the given {@link File}.
167      *
168      * @param fileInCache the {@link File}
169      */
170     private void deleteFile(File fileInCache) {
171         if (fileInCache.exists()) {
172             logger.debug("Deleting file '{}' from cache", fileInCache.getName());
173             fileInCache.delete();
174         } else {
175             logger.debug("File '{}' not found in cache", fileInCache.getName());
176         }
177     }
178
179     /**
180      * Removes all files from the cache.
181      */
182     public void clear() {
183         File[] filesInCache = cacheFolder.listFiles();
184         if (filesInCache != null && filesInCache.length > 0) {
185             logger.debug("Deleting all files from cache");
186             Arrays.stream(filesInCache).forEach(File::delete);
187         }
188     }
189
190     /**
191      * Removes expired files from the cache.
192      */
193     public void clearExpired() {
194         // exit if expiry is set to 0 (disabled)
195         if (expiry <= 0) {
196             return;
197         }
198         File[] filesInCache = cacheFolder.listFiles();
199         if (filesInCache != null && filesInCache.length > 0) {
200             logger.debug("Deleting expired files from cache");
201             Arrays.stream(filesInCache).filter(file -> isExpired(file)).forEach(File::delete);
202         }
203     }
204
205     /**
206      * Checks if the given {@link File} is expired.
207      *
208      * @param fileInCache the {@link File}
209      * @return <code>true</code> if the file is expired, <code>false</code> otherwise
210      */
211     private boolean isExpired(File fileInCache) {
212         // exit if expiry is set to 0 (disabled)
213         if (expiry <= 0) {
214             return false;
215         }
216         return expiry * ONE_DAY_IN_MILLIS < System.currentTimeMillis() - fileInCache.lastModified();
217     }
218
219     /**
220      * Returns the content of the file associated with the given key, if it is present.
221      *
222      * @param key the key whose associated file is to be returned
223      * @return the content of the file associated with the given key
224      */
225     public byte[] get(String key) {
226         return readFile(getUniqueFile(key));
227     }
228
229     /**
230      * Reads the content from the given {@link File}, if it is present.
231      *
232      * @param fileInCache the {@link File}
233      * @return the content of the file
234      */
235     private byte[] readFile(File fileInCache) {
236         if (fileInCache.exists()) {
237             logger.debug("Reading file '{}' from cache", fileInCache.getName());
238             // update time of last use
239             fileInCache.setLastModified(System.currentTimeMillis());
240             try {
241                 return Files.readAllBytes(fileInCache.toPath());
242             } catch (IOException e) {
243                 logger.warn("Could not read file '{}' from cache", fileInCache.getName(), e);
244             }
245         } else {
246             logger.debug("File '{}' not found in cache", fileInCache.getName());
247         }
248         return new byte[0];
249     }
250
251     /**
252      * Creates a unique {@link File} from the key with which the file is to be associated.
253      *
254      * @param key the key with which the file is to be associated
255      * @return unique file for the file associated with the given key
256      */
257     File getUniqueFile(String key) {
258         String uniqueFileName = getUniqueFileName(key);
259         if (FILES_IN_CACHE.containsKey(uniqueFileName)) {
260             return FILES_IN_CACHE.get(uniqueFileName);
261         } else {
262             String fileExtension = getFileExtension(key);
263             File fileInCache = new File(cacheFolder,
264                     uniqueFileName + (fileExtension == null ? "" : EXTENSION_SEPARATOR + fileExtension));
265             FILES_IN_CACHE.put(uniqueFileName, fileInCache);
266             return fileInCache;
267         }
268     }
269
270     /**
271      * Gets the extension of a file name.
272      *
273      * @param fileName the file name to retrieve the extension of
274      * @return the extension of the file or null if none exists
275      */
276     @Nullable
277     String getFileExtension(String fileName) {
278         int extensionPos = fileName.lastIndexOf(EXTENSION_SEPARATOR);
279         int lastSeparatorPos = Math.max(fileName.lastIndexOf(UNIX_SEPARATOR), fileName.lastIndexOf(WINDOWS_SEPARATOR));
280         return lastSeparatorPos > extensionPos ? null : fileName.substring(extensionPos + 1);
281     }
282
283     /**
284      * Creates a unique file name from the key with which the file is to be associated.
285      *
286      * @param key the key with which the file is to be associated
287      * @return unique file name for the file associated with the given key
288      */
289     String getUniqueFileName(String key) {
290         try {
291             MessageDigest md = MessageDigest.getInstance(MD5_ALGORITHM);
292             byte[] bytesOfKey = key.getBytes(StandardCharsets.UTF_8);
293             byte[] md5Hash = md.digest(bytesOfKey);
294             BigInteger bigInt = new BigInteger(1, md5Hash);
295             String fileNameHash = bigInt.toString(16);
296             // Now we need to zero pad it if you actually want the full 32 chars
297             while (fileNameHash.length() < 32) {
298                 fileNameHash = "0" + fileNameHash;
299             }
300             return fileNameHash;
301         } catch (NoSuchAlgorithmException ex) {
302             // should not happen
303             logger.error("Could not create MD5 hash for key '{}'", key, ex);
304             return key;
305         }
306     }
307 }