All Downloads are FREE. Search and download functionalities are using the official Maven repository.

javax0.jamal.tools.Cache Maven / Gradle / Ivy

package javax0.jamal.tools;

import javax0.jamal.api.EnvironmentVariables;

import java.io.BufferedReader;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStreamReader;
import java.net.URL;
import java.nio.charset.StandardCharsets;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.Map;
import java.util.Optional;
import java.util.Properties;

/**
 * A cache implementation that can store the strings in file which are downloaded from certain HTTPS URLs. The cache
 * elements never expire. The assumption is that the resources downloaded from a web page are versioned and the URL url
 * contains the version number. When the URL contains the literal {@code SNAPSHOT} it is not cached. In all other cases
 * we assume that the resource will NEVER change.
 *
 * 

* The structure of the files and directories are influenced by the structure of the Maven local repository. * *

* The cached files are stored under the directory {@code ~/.jamal/cache/}. In this directory there are two * subdirectories {@code https/} and {@code properties/}. The directory {@code https/} contains the cached files. The * directory {@code properties/} contain properties files. The properties contain certain information about the file in * the cache. */ public class Cache { /** * A cache entry. It contains the content File and the properties File and the Properties object. The properties are * loaded when the content is requested by the caller. There is no method to query the properties. The properties * files exist for debug purposes and currently contains the date and time when the entry was created and last * read. */ public static class Entry { private final File file; private final File propertiesFile; private final Properties properties; private final Properties effectiveProperties; private boolean propertiesLoaded = false; private Entry(File file, File propertiesFile) { this.file = file; this.propertiesFile = propertiesFile; this.properties = new Properties(); this.effectiveProperties = new Properties(); } /** * @return {@code true} if the file is not in the cache */ public boolean isMiss() { if (!file.exists()) { return true; } final var ttl = getProperty("ttl"); if (ttl == null) { return false; } final var write = getProperty("write"); return expiration(ttl, write) < System.currentTimeMillis(); } private long expiration(final String ttl, final String write) { final var ttlMillis = parseTtl(ttl); long writeMillis = 0L; try { writeMillis = Long.parseLong(write); } catch (NumberFormatException e) { // } return writeMillis + ttlMillis; } private long parseTtl(final String ttl) { final var sb = new StringBuilder(ttl); long seconds = 0L; try { seconds += chopSeconds(sb, "y", 365 * 24 * 60 * 60); seconds += chopSeconds(sb, "M", 31 * 24 * 60 * 60); seconds += chopSeconds(sb, "w", 37 * 24 * 60 * 60); seconds += chopSeconds(sb, "d", 24 * 60 * 60); seconds += chopSeconds(sb, "h", 60 * 60); seconds += chopSeconds(sb, "m", 60); seconds += chopSeconds(sb, "s", 1); final var value = sb.toString().trim(); if (value.length() > 0) { seconds += Long.parseLong(value); } } catch (NumberFormatException e) { // ignore } return seconds; } public long chopSeconds(StringBuilder sb, String unit, long seconds) { final var index = sb.indexOf(unit); if (index == -1) { return 0; } final var value = sb.substring(0, index).trim(); sb.delete(0, index + 1); return Long.parseLong(value) * seconds; } /** * @return the content of the cached file or {@code null} if the file is not in the cache or cannot be read. */ public StringBuilder getContent() { try { assertPropertiesAreLoaded(); if (file.exists()) { properties.put("read", "" + System.currentTimeMillis()); properties.put("read_formatted", now()); properties.put("count", "" + (Integer.parseInt(Optional.ofNullable((String) properties.get("count")).orElse("0")) + 1)); saveProperties(); return CachedHttpInput.readBufferedReader(getBufferedReader(file)); } else { return null; } } catch (IOException ioex) { return null; } } public String getProperty(String key) { try { assertPropertiesAreLoaded(); effectiveProperties.putAll(collectEffectiveProperties(propertiesFile.getParentFile())); effectiveProperties.putAll(properties); return effectiveProperties.getProperty(key); } catch (IOException ignored) { return null; } } private void assertPropertiesAreLoaded() throws IOException { if (!propertiesLoaded) { if (propertiesFile.exists()) { properties.load(new FileInputStream(propertiesFile)); } propertiesLoaded = true; } } private Properties collectEffectiveProperties(final File directory) { final var properties = new Properties(); if (!directory.getParentFile().equals(CACHE_ROOT_DIRECTORY)) { properties.putAll(collectEffectiveProperties(directory.getParentFile())); } final var dotPropertiesFile = new File(directory, ".properties"); if (dotPropertiesFile.exists()) { try { final var localProperties = new Properties(); localProperties.load(new FileInputStream(dotPropertiesFile)); properties.putAll(localProperties); } catch (IOException e) { // } } return properties; } /** * @return the current date and time formatted. Used to record the time in the properties file human readable. */ private static String now() { SimpleDateFormat format = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss.SSS Z"); return format.format(new Date()); } /** * See the documentation of {@link #save(String, Map[])} * * @param content to be saved into the cache file * @return the content itself */ StringBuilder save(StringBuilder content) { save(content.toString()); return content; } /** * Save the given content into a cache file. The saving may fail. In that case the failure will be silent and * does not throw an exception. This is designed that way not to prevent operation in case of a wrongly * configured cache. In that case the file will be downloaded each time instead of using the cache, but Jamal * will still work. * * @param content to be saved into the cache file * @param maps contains the key value pairs that will be saved into the cache properties file */ @SafeVarargs public final void save(String content, Map... maps) { if (cacheExists()) { try { assertPropertiesAreLoaded(); properties.put("write", "" + System.currentTimeMillis()); properties.put("write_formatted", now()); for (final var map : maps) { properties.putAll(map); } saveProperties(); //noinspection ResultOfMethodCallIgnored file.getParentFile().mkdirs(); try (final var fos = new FileOutputStream(file)) { fos.write(content.getBytes(StandardCharsets.UTF_8)); } } catch (IOException ignore) { } } } /** * Save the properties. In case there is an error then silently ignore it. For reasoning see {@link * #save(StringBuilder)}. Do not call this method. It is called from {@link #save(StringBuilder)}. (It is * private after all.) */ private void saveProperties() { //noinspection ResultOfMethodCallIgnored propertiesFile.getParentFile().mkdirs(); try { properties.store(new FileOutputStream(propertiesFile), " cache parameters of the entry " + file.getAbsolutePath()); } catch (IOException ignore) { } } } private static final String DEFAULT_CACHE_ROOT = "~/.jamal/cache/"; private static final String SNAPSHOT = "SNAPSHOT"; static final Entry NO_CACHE = new Entry(NonexistentFile.INSTANCE, NonexistentFile.INSTANCE); private /*non-final for test*/ static File CACHE_ROOT_DIRECTORY = new File( EnvironmentVariables.getenv(EnvironmentVariables.JAMAL_HTTPS_CACHE_ENV) .or(() -> Optional.of(DEFAULT_CACHE_ROOT)).map(FileTools::adjustedFileName).get()); /** * Get a cache entry for the given URL. * * @param url is the URL that the entry represents. This is the URL from where the original content was downloaded. * @return an entry. If there is no entry configured then it returns a pseudo entry that says that he entry is not * found. This same entry is returned in case the url contains the string {@code SNAPSHOT} all upper case. */ public static Entry getEntry(URL url) { if (!Cache.cacheExists() || url.toString().contains(SNAPSHOT)) { return Cache.NO_CACHE; } final var fn = convertUrl2FN(url); final var propfile = new StringBuilder(fn); fn.insert(0, "/https/"); fn.insert(0, CACHE_ROOT_DIRECTORY.getAbsolutePath()); propfile.insert(0, "/properties/"); propfile.insert(0, CACHE_ROOT_DIRECTORY.getAbsolutePath()); return new Entry(new File(fn.toString()), new File(propfile.toString())); } /** * Convert a URL to a file name. This file name will be used in the cache as a structured directory path and file * name. * * @param url the url to convert to file name * @return a directory structure and file name with the {@code /} separator. */ private static StringBuilder convertUrl2FN(URL url) { final var fn = new StringBuilder(); final var host = url.getHost(); final var path = url.getPath(); for (final var s : host.split("\\.", -1)) { fn.insert(0, s + "/"); } fn.append(path.substring(1)); return fn; } /** * Create a buffered reader that reads the file using the UTF-8 character set. * * @param file is going to be read by the returned reader. * @return the reader * @throws IOException if the file cannot be opened. */ private static BufferedReader getBufferedReader(File file) throws IOException { return new BufferedReader(new InputStreamReader(new FileInputStream(file), StandardCharsets.UTF_8)); } /** * @return {@code true} is the cache is properly configured and exists. The cache is used if the cache directory * exists. It has to be created manually. The directories under it are recursively created when a file is cached, * but the cache directory has to be manually created. That way the cache can be configured not to be used simply * deleting the cache directory. (Not simply emptying!) */ static boolean cacheExists() { return CACHE_ROOT_DIRECTORY.exists() && CACHE_ROOT_DIRECTORY.isDirectory(); } /** * A file object that says the file does not exist. On anything else it returns what is returned by {@code new * File("")}. This is a singleton, use the {@code INSTANCE} field. */ private static class NonexistentFile extends File { static final File INSTANCE = new NonexistentFile(""); NonexistentFile(String pathname) { super(pathname); } @Override public boolean exists() { return false; } } }





© 2015 - 2024 Weber Informatics LLC | Privacy Policy