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