2 * Copyright (c) 2010-2020 Contributors to the openHAB project
4 * See the NOTICE file(s) distributed with this work for additional
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
11 * SPDX-License-Identifier: EPL-2.0
13 package org.openhab.binding.chromecast.internal.utils;
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;
25 import java.util.concurrent.ConcurrentHashMap;
26 import java.util.concurrent.TimeUnit;
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;
35 * This is a simple file based cache implementation.
37 * @author Christoph Weitkamp - Initial contribution
40 public class ByteArrayFileCache {
42 private final Logger logger = LoggerFactory.getLogger(ByteArrayFileCache.class);
44 private static final String MD5_ALGORITHM = "MD5";
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 = '\\';
51 private final File cacheFolder;
53 static final long ONE_DAY_IN_MILLIS = TimeUnit.DAYS.toMillis(1);
54 private int expiry = 0;
56 private static final Map<String, File> FILES_IN_CACHE = new ConcurrentHashMap<>();
59 * Creates a new {@link ByteArrayFileCache} instance for a service. Creates a <code>cache</code> folder under
60 * <code>$userdata/cache/$servicePID</code>.
62 * @param servicePID PID of the service
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());
72 logger.debug("Using cache folder '{}'", cacheFolder.getAbsolutePath());
76 * Creates a new {@link ByteArrayFileCache} instance for a service. Creates a <code>cache</code> folder under
77 * <code>$userdata/cache/$servicePID/</code>.
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.
83 public ByteArrayFileCache(String servicePID, int expiry) {
86 throw new IllegalArgumentException("Cache expiration time must be greater than or equal to 0");
92 * Adds a file to the cache. If the cache previously contained a file for the key, the old file is replaced by the
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
98 public void put(String key, byte[] content) {
99 writeFile(getUniqueFile(key), content);
103 * Adds a file to the cache.
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
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());
115 writeFile(fileInCache, content);
120 * Adds a file to the cache and returns the content of the file.
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
126 public byte[] putIfAbsentAndGet(String key, byte[] content) {
127 putIfAbsent(key, content);
133 * Writes the given content to the given {@link File}.
135 * @param fileInCache the {@link File}
136 * @param content the content to be written
138 private void writeFile(File fileInCache, byte[] content) {
139 logger.debug("Caching file '{}'", fileInCache.getName());
141 Files.write(fileInCache.toPath(), content);
142 } catch (IOException e) {
143 logger.warn("Could not write file '{}' to cache", fileInCache.getName(), e);
148 * Checks if the key is present in the cache.
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
153 public boolean containsKey(String key) {
154 return getUniqueFile(key).exists();
158 * Removes the file associated with the given key from the cache.
160 * @param key the key whose associated file is to be removed
162 public void remove(String key) {
163 deleteFile(getUniqueFile(key));
167 * Deletes the given {@link File}.
169 * @param fileInCache the {@link File}
171 private void deleteFile(File fileInCache) {
172 if (fileInCache.exists()) {
173 logger.debug("Deleting file '{}' from cache", fileInCache.getName());
174 fileInCache.delete();
176 logger.debug("File '{}' not found in cache", fileInCache.getName());
181 * Removes all files from the cache.
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);
192 * Removes expired files from the cache.
194 public void clearExpired() {
195 // exit if expiry is set to 0 (disabled)
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);
207 * Checks if the given {@link File} is expired.
209 * @param fileInCache the {@link File}
210 * @return <code>true</code> if the file is expired, <code>false</code> otherwise
212 private boolean isExpired(File fileInCache) {
213 // exit if expiry is set to 0 (disabled)
217 return expiry * ONE_DAY_IN_MILLIS < System.currentTimeMillis() - fileInCache.lastModified();
221 * Returns the content of the file associated with the given key, if it is present.
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
228 public byte[] get(String key) throws FileNotFoundException, IOException {
229 return readFile(getUniqueFile(key));
233 * Reads the content from the given {@link File}, if it is present.
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
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());
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()));
252 logger.debug("File '{}' not found in cache", fileInCache.getName());
253 throw new FileNotFoundException(String.format("File '%s' not found in cache", fileInCache.getName()));
258 * Creates a unique {@link File} from the key with which the file is to be associated.
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
263 File getUniqueFile(String key) {
264 String uniqueFileName = getUniqueFileName(key);
265 if (FILES_IN_CACHE.containsKey(uniqueFileName)) {
266 return FILES_IN_CACHE.get(uniqueFileName);
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);
277 * Gets the extension of a file name.
279 * @param fileName the file name to retrieve the extension of
280 * @return the extension of the file or null if none exists
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("\\?.*$", "");
290 * Creates a unique file name from the key with which the file is to be associated.
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
295 String getUniqueFileName(String key) {
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) {
301 logger.error("Could not create MD5 hash for key '{}'", key, ex);