diff --git a/src/main/java/org/cryptomator/common/LazyProcessedProperties.java b/src/main/java/org/cryptomator/common/LazyProcessedProperties.java new file mode 100644 index 000000000..97248035a --- /dev/null +++ b/src/main/java/org/cryptomator/common/LazyProcessedProperties.java @@ -0,0 +1,255 @@ +package org.cryptomator.common; + +import org.jetbrains.annotations.Nullable; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.io.PrintStream; +import java.io.PrintWriter; +import java.io.Reader; +import java.io.Writer; +import java.nio.charset.Charset; +import java.util.Collection; +import java.util.Enumeration; +import java.util.InvalidPropertiesFormatException; +import java.util.Map; +import java.util.Properties; +import java.util.Set; +import java.util.function.BiConsumer; +import java.util.function.BiFunction; +import java.util.function.Function; +import java.util.regex.Pattern; + +public class LazyProcessedProperties extends Properties { + + private static final Logger LOG = LoggerFactory.getLogger(LazyProcessedProperties.class); + + //Template and env _need_ to be instance variables, otherwise they might not be initialized at access time + private final Pattern template = Pattern.compile("@\\{(\\w+)}"); + private final Map env = System.getenv(); + private final Properties delegate; + + public LazyProcessedProperties(Properties props) { + this.delegate = props; + } + + @Override + public String getProperty(String key) { + var value = delegate.getProperty(key); + if (key.startsWith("cryptomator.") && value != null) { + return process(value); + } else { + return value; + } + } + + @Override + public String getProperty(String key, String defaultValue) { + var value = delegate.getProperty(key, defaultValue); + if (key.startsWith("cryptomator.") && value != null) { + return process(value); + } else { + return value; + } + } + + String process(String value) { + return template.matcher(value).replaceAll(match -> // + switch (match.group(1)) { + case "appdir" -> resolveFrom("APPDIR", Source.ENV); + case "appdata" -> resolveFrom("APPDATA", Source.ENV); + case "localappdata" -> resolveFrom("LOCALAPPDATA", Source.ENV); + case "userhome" -> resolveFrom("user.home", Source.PROPS); + default -> { + LOG.warn("Unknown variable @{{}} in property value {}.", match.group(), value); + yield match.group(); + } + }); + } + + private String resolveFrom(String key, Source src) { + var val = switch (src) { + case ENV -> env.get(key); + case PROPS -> delegate.getProperty(key); + }; + if (val == null) { + LOG.warn("Variable {} used for substitution not found in {}. Replaced with empty string.", key, src); + return ""; + } else { + return val.replace("\\", "\\\\"); + } + } + + private enum Source { + ENV, + PROPS; + } + + @Override + public Object setProperty(String key, String value) { + return delegate.setProperty(key, value); + } + + //auto generated + @Override + public void load(Reader reader) throws IOException {delegate.load(reader);} + + @Override + public void load(InputStream inStream) throws IOException {delegate.load(inStream);} + + @Override + @Deprecated + public void save(OutputStream out, String comments) {delegate.save(out, comments);} + + @Override + public void store(Writer writer, String comments) throws IOException {delegate.store(writer, comments);} + + @Override + public void store(OutputStream out, @Nullable String comments) throws IOException {delegate.store(out, comments);} + + @Override + public void loadFromXML(InputStream in) throws IOException, InvalidPropertiesFormatException {delegate.loadFromXML(in);} + + @Override + public void storeToXML(OutputStream os, String comment) throws IOException {delegate.storeToXML(os, comment);} + + @Override + public void storeToXML(OutputStream os, String comment, String encoding) throws IOException {delegate.storeToXML(os, comment, encoding);} + + @Override + public void storeToXML(OutputStream os, String comment, Charset charset) throws IOException {delegate.storeToXML(os, comment, charset);} + + @Override + public Enumeration propertyNames() {return delegate.propertyNames();} + + @Override + public Set stringPropertyNames() {return delegate.stringPropertyNames();} + + @Override + public void list(PrintStream out) {delegate.list(out);} + + @Override + public void list(PrintWriter out) {delegate.list(out);} + + @Override + public int size() {return delegate.size();} + + @Override + public boolean isEmpty() {return delegate.isEmpty();} + + @Override + public Enumeration keys() {return delegate.keys();} + + @Override + public Enumeration elements() {return delegate.elements();} + + @Override + public boolean contains(Object value) {return delegate.contains(value);} + + @Override + public boolean containsValue(Object value) {return delegate.containsValue(value);} + + @Override + public boolean containsKey(Object key) {return delegate.containsKey(key);} + + @Override + public Object get(Object key) {return delegate.get(key);} + + @Override + public Object put(Object key, Object value) {return delegate.put(key, value);} + + @Override + public Object remove(Object key) {return delegate.remove(key);} + + @Override + public void putAll(Map t) {delegate.putAll(t);} + + @Override + public void clear() {delegate.clear();} + + @Override + public String toString() {return delegate.toString();} + + @Override + public Set keySet() {return delegate.keySet();} + + @Override + public Collection values() {return delegate.values();} + + @Override + public Set> entrySet() {return delegate.entrySet();} + + @Override + public boolean equals(Object o) {return delegate.equals(o);} + + @Override + public int hashCode() {return delegate.hashCode();} + + @Override + public Object getOrDefault(Object key, Object defaultValue) {return delegate.getOrDefault(key, defaultValue);} + + @Override + public void forEach(BiConsumer action) {delegate.forEach(action);} + + @Override + public void replaceAll(BiFunction function) {delegate.replaceAll(function);} + + @Override + public Object putIfAbsent(Object key, Object value) {return delegate.putIfAbsent(key, value);} + + @Override + public boolean remove(Object key, Object value) {return delegate.remove(key, value);} + + @Override + public boolean replace(Object key, Object oldValue, Object newValue) {return delegate.replace(key, oldValue, newValue);} + + @Override + public Object replace(Object key, Object value) {return delegate.replace(key, value);} + + @Override + public Object computeIfAbsent(Object key, Function mappingFunction) {return delegate.computeIfAbsent(key, mappingFunction);} + + @Override + public Object computeIfPresent(Object key, BiFunction remappingFunction) {return delegate.computeIfPresent(key, remappingFunction);} + + @Override + public Object compute(Object key, BiFunction remappingFunction) {return delegate.compute(key, remappingFunction);} + + @Override + public Object merge(Object key, Object value, BiFunction remappingFunction) {return delegate.merge(key, value, remappingFunction);} + + @Override + public Object clone() {return delegate.clone();} + + public static Map of() {return Map.of();} + + public static Map of(K k1, V v1) {return Map.of(k1, v1);} + + public static Map of(K k1, V v1, K k2, V v2) {return Map.of(k1, v1, k2, v2);} + + public static Map of(K k1, V v1, K k2, V v2, K k3, V v3) {return Map.of(k1, v1, k2, v2, k3, v3);} + + public static Map of(K k1, V v1, K k2, V v2, K k3, V v3, K k4, V v4) {return Map.of(k1, v1, k2, v2, k3, v3, k4, v4);} + + public static Map of(K k1, V v1, K k2, V v2, K k3, V v3, K k4, V v4, K k5, V v5) {return Map.of(k1, v1, k2, v2, k3, v3, k4, v4, k5, v5);} + + public static Map of(K k1, V v1, K k2, V v2, K k3, V v3, K k4, V v4, K k5, V v5, K k6, V v6) {return Map.of(k1, v1, k2, v2, k3, v3, k4, v4, k5, v5, k6, v6);} + + public static Map of(K k1, V v1, K k2, V v2, K k3, V v3, K k4, V v4, K k5, V v5, K k6, V v6, K k7, V v7) {return Map.of(k1, v1, k2, v2, k3, v3, k4, v4, k5, v5, k6, v6, k7, v7);} + + public static Map of(K k1, V v1, K k2, V v2, K k3, V v3, K k4, V v4, K k5, V v5, K k6, V v6, K k7, V v7, K k8, V v8) {return Map.of(k1, v1, k2, v2, k3, v3, k4, v4, k5, v5, k6, v6, k7, v7, k8, v8);} + + public static Map of(K k1, V v1, K k2, V v2, K k3, V v3, K k4, V v4, K k5, V v5, K k6, V v6, K k7, V v7, K k8, V v8, K k9, V v9) {return Map.of(k1, v1, k2, v2, k3, v3, k4, v4, k5, v5, k6, v6, k7, v7, k8, v8, k9, v9);} + + public static Map of(K k1, V v1, K k2, V v2, K k3, V v3, K k4, V v4, K k5, V v5, K k6, V v6, K k7, V v7, K k8, V v8, K k9, V v9, K k10, V v10) {return Map.of(k1, v1, k2, v2, k3, v3, k4, v4, k5, v5, k6, v6, k7, v7, k8, v8, k9, v9, k10, v10);} + + @SafeVarargs + public static Map ofEntries(Map.Entry... entries) {return Map.ofEntries(entries);} + + public static Map.Entry entry(K k, V v) {return Map.entry(k, v);} + + public static Map copyOf(Map map) {return Map.copyOf(map);} +} diff --git a/src/main/java/org/cryptomator/common/PropertiesPreprocessor.java b/src/main/java/org/cryptomator/common/PropertiesPreprocessor.java deleted file mode 100644 index 683b04aa2..000000000 --- a/src/main/java/org/cryptomator/common/PropertiesPreprocessor.java +++ /dev/null @@ -1,69 +0,0 @@ -package org.cryptomator.common; - -import org.jetbrains.annotations.VisibleForTesting; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import java.util.Map; -import java.util.regex.Pattern; - -public class PropertiesPreprocessor { - - private static final Logger LOG = LoggerFactory.getLogger(PropertiesPreprocessor.class); - private static final Pattern TEMPLATE = Pattern.compile("@\\{(\\w+)}"); - private static final LoggingEnvironment ENV = new LoggingEnvironment(System.getenv(), LOG); - - public static void run() { - var properties = System.getProperties(); - properties.stringPropertyNames().stream() // - .filter(s -> s.startsWith("cryptomator.")) // - .forEach(key -> { - var value = properties.getProperty(key); - var newValue = process(value); - if(! value.equals(newValue)) { - LOG.info("Changed property {} from {} to {}.", key, value, newValue); - } - properties.setProperty(key, newValue); - }); - LOG.info("Preprocessed cryptomator properties."); - } - - @VisibleForTesting - static String process(String value) { - return TEMPLATE.matcher(value).replaceAll(match -> // - switch (match.group(1)) { - case "appdir" -> ENV.get("APPDIR"); - case "appdata" -> ENV.get("APPDATA"); - case "localappdata" -> ENV.get("LOCALAPPDATA"); - case "userhome" -> System.getProperty("user.home"); - default -> { - LOG.warn("Found unknown variable @{{}} in property value {}.", match.group(), value); - yield match.group(); - } - }); - } - - private static class LoggingEnvironment { - - private final Map env; - private final Logger logger; - - LoggingEnvironment(Map env, Logger logger) { - this.env = env; - this.logger = logger; - } - - String get(String key) { - var val = env.get(key); - if (val == null) { - logger.warn("Variable {} used for substitution not found in environment", key); - return ""; - } else { - return val; - } - } - - } - - -} diff --git a/src/main/java/org/cryptomator/launcher/Cryptomator.java b/src/main/java/org/cryptomator/launcher/Cryptomator.java index f08d70c37..0985ee33e 100644 --- a/src/main/java/org/cryptomator/launcher/Cryptomator.java +++ b/src/main/java/org/cryptomator/launcher/Cryptomator.java @@ -9,7 +9,7 @@ import com.google.common.util.concurrent.ThreadFactoryBuilder; import dagger.Lazy; import org.apache.commons.lang3.SystemUtils; import org.cryptomator.common.Environment; -import org.cryptomator.common.PropertiesPreprocessor; +import org.cryptomator.common.LazyProcessedProperties; import org.cryptomator.common.ShutdownHook; import org.cryptomator.ipc.IpcCommunicator; import org.cryptomator.logging.DebugMode; @@ -32,8 +32,16 @@ public class Cryptomator { private static final long STARTUP_TIME = System.currentTimeMillis(); // DaggerCryptomatorComponent gets generated by Dagger. // Run Maven and include target/generated-sources/annotations in your IDE. - private static final CryptomatorComponent CRYPTOMATOR_COMPONENT = DaggerCryptomatorComponent.factory().create(STARTUP_TIME); - private static final Logger LOG = LoggerFactory.getLogger(Cryptomator.class); + + static { + var lazyProcessedProps = new LazyProcessedProperties(System.getProperties()); + System.setProperties(lazyProcessedProps); + CRYPTOMATOR_COMPONENT = DaggerCryptomatorComponent.factory().create(STARTUP_TIME); + LOG = LoggerFactory.getLogger(Cryptomator.class); + } + + private static final CryptomatorComponent CRYPTOMATOR_COMPONENT; + private static final Logger LOG; private final DebugMode debugMode; private final SupportedLanguages supportedLanguages; @@ -64,7 +72,6 @@ public class Cryptomator { System.out.printf("Cryptomator version %s (build %s)%n", appVer, buildNumber); return; } - PropertiesPreprocessor.run(); int exitCode = CRYPTOMATOR_COMPONENT.application().run(args); LOG.info("Exit {}", exitCode); System.exit(exitCode); // end remaining non-daemon threads. diff --git a/src/main/java/org/cryptomator/logging/LogbackConfigurator.java b/src/main/java/org/cryptomator/logging/LogbackConfigurator.java index 511599132..41275d073 100644 --- a/src/main/java/org/cryptomator/logging/LogbackConfigurator.java +++ b/src/main/java/org/cryptomator/logging/LogbackConfigurator.java @@ -14,10 +14,12 @@ import ch.qos.logback.core.rolling.FixedWindowRollingPolicy; import ch.qos.logback.core.rolling.RollingFileAppender; import ch.qos.logback.core.spi.ContextAwareBase; import ch.qos.logback.core.util.FileSize; -import org.cryptomator.common.Environment; +import org.cryptomator.common.LazyProcessedProperties; +import java.nio.file.Files; import java.nio.file.Path; import java.util.Map; +import java.util.Optional; public class LogbackConfigurator extends ContextAwareBase implements Configurator { @@ -56,8 +58,10 @@ public class LogbackConfigurator extends ContextAwareBase implements Configurato @Override public ExecutionStatus configure(LoggerContext context) { - var useCustomCfg = Environment.getInstance().useCustomLogbackConfig(); - var logDir = Environment.getInstance().getLogDir().orElse(null); + //we need to preprocess those, because every other class has a dependency to logging, none are initialized yet + var processedProps = new LazyProcessedProperties(System.getProperties()); + var useCustomCfg = Optional.ofNullable(processedProps.getProperty("logback.configurationFile")).map(s -> Files.exists(Path.of(s))).orElse(false); + var logDir = Optional.ofNullable(processedProps.getProperty("cryptomator.logDir")).map(Path::of).orElse(null); if (useCustomCfg) { addInfo("Using external logback configuration file."); diff --git a/src/test/java/org/cryptomator/common/PropertiesPreprocessorTest.java b/src/test/java/org/cryptomator/common/LazyProcessedPropertiesTest.java similarity index 73% rename from src/test/java/org/cryptomator/common/PropertiesPreprocessorTest.java rename to src/test/java/org/cryptomator/common/LazyProcessedPropertiesTest.java index eee900f9c..7b7e2948f 100644 --- a/src/test/java/org/cryptomator/common/PropertiesPreprocessorTest.java +++ b/src/test/java/org/cryptomator/common/LazyProcessedPropertiesTest.java @@ -1,12 +1,11 @@ package org.cryptomator.common; import org.junit.jupiter.api.Assertions; -import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.CsvSource; -import org.mockito.Mockito; -public class PropertiesPreprocessorTest { +public class LazyProcessedPropertiesTest { @ParameterizedTest @CsvSource(value = """ @@ -17,7 +16,8 @@ public class PropertiesPreprocessorTest { Longer @{appdir} text with @{appdir}., Longer text with . """) public void test(String propertyValue, String expected) { - var result = PropertiesPreprocessor.process(propertyValue); + LazyProcessedProperties inTest = new LazyProcessedProperties(System.getProperties()); + var result = inTest.process(propertyValue); Assertions.assertEquals(result, expected); }