001package co.codewizards.cloudstore.core.config;
002
003import static co.codewizards.cloudstore.core.util.Util.*;
004
005import java.io.File;
006import java.io.IOException;
007import java.io.InputStream;
008import java.lang.ref.SoftReference;
009import java.lang.ref.WeakReference;
010import java.util.Iterator;
011import java.util.LinkedHashSet;
012import java.util.LinkedList;
013import java.util.Map;
014import java.util.Properties;
015import java.util.WeakHashMap;
016
017import org.slf4j.Logger;
018import org.slf4j.LoggerFactory;
019
020import co.codewizards.cloudstore.core.io.LockFile;
021import co.codewizards.cloudstore.core.io.LockFileFactory;
022import co.codewizards.cloudstore.core.repo.local.LocalRepoHelper;
023import co.codewizards.cloudstore.core.util.IOUtil;
024
025/**
026 * Configuration of CloudStore supporting inheritance of settings.
027 * <p>
028 * There is one {@code Config} instance available (lazily created, cached temporarily) for every
029 * directory and every file in a repository. Each {@code Config} inherits the settings from the
030 * parent-directory, if not explicitly overwritten.
031 * <p>
032 * The configuration is based on {@link Properties} files. Every property file is optional. If it
033 * does not exist, all settings are inherited. If it does exist, only those properties contained in
034 * the file are overriden. All properties not contained in the file are still inherited. Inheritance
035 * is thus applicable on every individual property.
036 * <p>
037 * Modifications, deletions, creations of properties files are detected during runtime (pretty immediately).
038 * Note, that this detection is based on the files' timestamps. Since most file systems have a granularity
039 * of 1 second (some even 2) for the last-modified-timestamp, multiple modifications in the same second might
040 * not be detected.
041 * <p>
042 * There is a global properties file in the user's home directory (or wherever {@link ConfigDir}
043 * points to): <code>/home/tomcat/.cloudstore/cloudstore.properties</code>
044 * <p>
045 * Additionally, every directory can optionally contain the following files:
046 * <ol>
047 * <li><code>.cloudstore.properties</code>
048 * <li><code>cloudstore.properties</code>
049 * <li><code>.${anyFileName}.cloudstore.properties</code>
050 * <li><code>${anyFileName}.cloudstore.properties</code>
051 * </ol>
052 * <p>
053 * The files 1. and 2. are applicable to the entire directory and all sub-directories and files in it.
054 * Usually, on GNU/Linux people will prefer 1., but when using Windows, files starting with a "." are
055 * sometimes a bit hard to deal with. Therefore, we support both. The file 2. overrides the settings of file 1..
056 * <p>
057 * The files 3. and 4. are applicable only to the file <code>${anyFileName}</code>. Thus, if you want
058 * to set special behaviour for the file <code>example.db</code> only, you can create the file
059 * <code>.example.db.cloudstore.properties</code> in the same directory.
060 *
061 * @author Marco หงุ่ยตระกูล-Schulze - marco at codewizards dot co
062 */
063public class Config {
064        private static final Logger logger = LoggerFactory.getLogger(Config.class);
065
066        private static final long fileRefsCleanPeriod = 60000L;
067        private static long fileRefsCleanLastTimestamp;
068
069        private static final String PROPERTIES_FILE_NAME_FOR_DIRECTORY_HIDDEN = ".cloudstore.properties";
070        private static final String PROPERTIES_FILE_NAME_FOR_DIRECTORY_VISIBLE = "cloudstore.properties";
071
072        private static final String PROPERTIES_FILE_FORMAT_FOR_FILE_HIDDEN = ".%s.cloudstore.properties";
073        private static final String PROPERTIES_FILE_FORMAT_FOR_FILE_VISIBLE = "%s.cloudstore.properties";
074
075        private static final String TRUE_STRING = Boolean.TRUE.toString();
076        private static final String FALSE_STRING = Boolean.FALSE.toString();
077
078        /**
079         * Prefix used for system properties overriding configuration entries.
080         * <p>
081         * Every property in the configuration (i.e. in its properties files) can be overridden
082         * by a corresponding system property. The system property must be prefixed.
083         * <p>
084         * For example, to override the configuration property with the key "deferrableExecutor.timeout",
085         * you can pass the system property "cloudstore.deferrableExecutor.timeout" to the JVM. If the
086         * system property exists, the configuration is not consulted, but the system property value is
087         * used as shortcut.
088         */
089        public static final String SYSTEM_PROPERTY_PREFIX = "cloudstore.";
090
091        private static final LinkedHashSet<File> fileHardRefs = new LinkedHashSet<>();
092        private static final int fileHardRefsMaxSize = 30;
093        /**
094         * {@link SoftReference}s to the files used in {@link #file2Config}.
095         * <p>
096         * There is no {@code SoftHashMap}, hence we use a WeakHashMap combined with the {@code SoftReference}s here.
097         * @see #file2Config
098         */
099        private static final LinkedList<SoftReference<File>> fileSoftRefs = new LinkedList<>();
100        /**
101         * @see #fileSoftRefs
102         */
103        private static final Map<File, Config> file2Config = new WeakHashMap<File, Config>();
104
105        private static final class ConfigHolder {
106                public static final Config instance = new Config(
107                                null, null,
108                                new File[] { new File(ConfigDir.getInstance().getFile(), PROPERTIES_FILE_NAME_FOR_DIRECTORY_VISIBLE) });
109        }
110
111        private final Config parentConfig;
112        private final WeakReference<File> fileRef;
113        protected final File[] propertiesFiles;
114        private final long[] propertiesFilesLastModified;
115        private final Properties properties;
116
117        private static final Object classMutex = Config.class;
118        private final Object instanceMutex;
119
120        private Config(Config parentConfig, File file, File [] propertiesFiles) {
121                this.parentConfig = parentConfig;
122
123                if (parentConfig == null)
124                        fileRef = null;
125                else
126                        fileRef = new WeakReference<File>(assertNotNull("file", file));
127
128                this.propertiesFiles = assertNotNullAndNoNullElement("propertiesFiles", propertiesFiles);
129                properties = new Properties(parentConfig == null ? null : parentConfig.properties);
130                propertiesFilesLastModified = new long[propertiesFiles.length];
131                instanceMutex = properties;
132
133                // Create the default global configuration (it's an empty template with some comments).
134                if (parentConfig == null && !propertiesFiles[0].exists()) {
135                        try {
136                                IOUtil.copyResource(Config.class, "/" + PROPERTIES_FILE_NAME_FOR_DIRECTORY_VISIBLE, propertiesFiles[0]);
137                        } catch (IOException e) {
138                                throw new RuntimeException(e);
139                        }
140                }
141        }
142
143        /**
144         * Get the directory or file for which this Config instance is responsible.
145         * @return the directory or file for which this Config instance is responsible. Might be <code>null</code>, if already
146         * garbage-collected or if this is the root-parent-Config. We try to make garbage-collection extremely unlikely
147         * as long as the Config is held in memory.
148         */
149        protected File getFile() {
150                return fileRef == null ? null : fileRef.get();
151        }
152
153        private static void cleanFileRefs() {
154                synchronized (classMutex) {
155                        if (System.currentTimeMillis() - fileRefsCleanLastTimestamp < fileRefsCleanPeriod)
156                                return;
157
158                        for (Iterator<SoftReference<File>> it = fileSoftRefs.iterator(); it.hasNext(); ) {
159                                SoftReference<File> fileRef = it.next();
160                                if (fileRef.get() == null)
161                                        it.remove();
162                        }
163                        fileRefsCleanLastTimestamp = System.currentTimeMillis();
164                }
165        }
166
167        /**
168         * Gets the global {@code Config} for the current user.
169         * @return the global {@code Config} for the current user. Never <code>null</code>.
170         */
171        public static Config getInstance() {
172                return ConfigHolder.instance;
173        }
174
175        /**
176         * Gets the {@code Config} for the given {@code directory}.
177         * @param directory a directory inside a repository. Must not be <code>null</code>.
178         * The directory does not need to exist (it may be created later).
179         * @return the {@code Config} for the given {@code directory}. Never <code>null</code>.
180         */
181        public static Config getInstanceForDirectory(final File directory) {
182                return getInstance(directory, true);
183        }
184
185        /**
186         * Gets the {@code Config} for the given {@code file}.
187         * @param file a file inside a repository. Must not be <code>null</code>.
188         * The file does not need to exist (it may be created later).
189         * @return the {@code Config} for the given {@code file}. Never <code>null</code>.
190         */
191        public static Config getInstanceForFile(final File file) {
192                return getInstance(file, false);
193        }
194
195        private static Config getInstance(final File file, final boolean isDirectory) {
196                assertNotNull("file", file);
197                cleanFileRefs();
198
199                File config_file = null;
200                Config config;
201                synchronized (classMutex) {
202                        config = file2Config.get(file);
203                        if (config != null) {
204                                config_file = config.getFile();
205                                if (config_file == null) // very unlikely, but it actually *can* happen.
206                                        config = null; // we try to make it extremely probable that the Config we return does have a valid file reference.
207                        }
208
209                        if (config == null) {
210                                final File localRoot = LocalRepoHelper.getLocalRootContainingFile(file);
211                                if (localRoot == null)
212                                        throw new IllegalArgumentException("file is not inside a repository: " + file.getAbsolutePath());
213
214                                final Config parentConfig = localRoot == file ? getInstance() : getInstance(file.getParentFile(), true);
215                                config = new Config(parentConfig, file, createPropertiesFiles(file, isDirectory));
216                                file2Config.put(file, config);
217                                fileSoftRefs.add(new SoftReference<File>(file));
218                                config_file = config.getFile();
219                        }
220                        assertNotNull("config_file", config_file);
221                }
222                refreshFileHardRefAndCleanOldHardRefs(config_file);
223                return config;
224        }
225
226        private static File[] createPropertiesFiles(final File file, final boolean isDirectory) {
227                if (isDirectory) {
228                        return new File[] {
229                                new File(file, PROPERTIES_FILE_NAME_FOR_DIRECTORY_HIDDEN),
230                                new File(file, PROPERTIES_FILE_NAME_FOR_DIRECTORY_VISIBLE)
231                        };
232                }
233                else {
234                        return new File[] {
235                                new File(file.getParentFile(), String.format(PROPERTIES_FILE_FORMAT_FOR_FILE_HIDDEN, file.getName())),
236                                new File(file.getParentFile(), String.format(PROPERTIES_FILE_FORMAT_FOR_FILE_VISIBLE, file.getName()))
237                        };
238                }
239        }
240
241        private Config readIfNeeded() {
242                synchronized (instanceMutex) {
243                        for (int i = 0; i < propertiesFiles.length; i++) {
244                                final File propertiesFile = propertiesFiles[i];
245                                final long lastModified = propertiesFilesLastModified[i];
246                                if (propertiesFile.lastModified() != lastModified) {
247                                        read();
248                                        break;
249                                }
250                        }
251                }
252
253                if (parentConfig != null)
254                        parentConfig.readIfNeeded();
255
256                return this;
257        }
258
259        private void read() {
260                synchronized (instanceMutex) {
261                        logger.trace("read: Entered instanceMutex.");
262                        try {
263                                properties.clear();
264                                for (int i = 0; i < propertiesFiles.length; i++) {
265                                        final File propertiesFile = propertiesFiles[i];
266                                        logger.debug("read: Reading propertiesFile '{}'.", propertiesFile.getAbsolutePath());
267                                        final long lastModified = propertiesFile.lastModified(); // is 0 for non-existing file
268                                        if (propertiesFile.exists()) { // prevent the properties file from being modified while we're reading it.
269                                                LockFile lockFile = LockFileFactory.getInstance().acquire(propertiesFile, 10000); // TODO maybe system property for timeout?
270                                                try {
271                                                        final InputStream in = lockFile.createInputStream();
272                                                        try {
273                                                                properties.load(in);
274                                                        } finally {
275                                                                in.close();
276                                                        }
277                                                } finally {
278                                                        lockFile.release();
279                                                }
280                                        }
281                                        propertiesFilesLastModified[i] = lastModified;
282                                }
283                        } catch (IOException e) {
284                                properties.clear();
285                                throw new RuntimeException(e);
286                        }
287                }
288        }
289
290        /**
291         * Gets the property identified by the given key.
292         * <p>
293         * This method directly delegates to {@link Properties#getProperty(String, String)}.
294         * Thus, an empty String in the internal {@code Properties} is returned instead of the
295         * given {@code defaultValue}. The {@code defaultValue} is only returned, if neither
296         * the internal {@code Properties} of this {@code Config} nor any of its parents contains
297         * the entry.
298         * <p>
299         * <b>Important:</b> This is often not the desired behaviour. You might want to use
300         * {@link #getPropertyAsNonEmptyTrimmedString(String, String)} instead!
301         * <p>
302         * Every property can be overwritten by a system property prefixed with {@value #SYSTEM_PROPERTY_PREFIX}.
303         * If - for example - the key "updater.force" is to be read and a system property
304         * named "cloudstore.updater.force" is set, this system property is returned instead!
305         * @param key the key identifying the property. Must not be <code>null</code>.
306         * @param defaultValue the default value to fall back to, if neither this {@code Config}'s
307         * internal {@code Properties} nor any of its parents contains a matching entry.
308         * May be <code>null</code>.
309         * @return the property's value. Never <code>null</code> unless {@code defaultValue} is <code>null</code>.
310         * @see #getPropertyAsNonEmptyTrimmedString(String, String)
311         */
312        public String getProperty(final String key, final String defaultValue) {
313                assertNotNull("key", key);
314                refreshFileHardRefAndCleanOldHardRefs();
315
316                final String sysPropKey = SYSTEM_PROPERTY_PREFIX + key;
317                final String sysPropVal = System.getProperty(sysPropKey);
318                if (sysPropVal != null) {
319                        logger.debug("getProperty: System property with key='{}' and value='{}' overrides config (config is not queried).", sysPropKey, sysPropVal);
320                        return sysPropVal;
321                }
322                logger.debug("getProperty: System property with key='{}' is not set (config is queried next).", sysPropKey);
323
324                synchronized (instanceMutex) {
325                        readIfNeeded();
326                        return properties.getProperty(key, defaultValue);
327                }
328        }
329
330        /**
331         * Gets the property identified by the given key; {@linkplain String#trim() trimmed}.
332         * <p>
333         * In contrast to {@link #getProperty(String, String)}, this method falls back to the given
334         * {@code defaultValue}, if the internal {@code Properties} contains an empty {@code String}
335         * (after trimming) as value for the given {@code key}.
336         * <p>
337         * It therefore means that a value set to an empty {@code String} in the properties file means
338         * to use the program's default instead. It is therefore consistent with
339         * {@link #getPropertyAsLong(String, long)} and all other {@code getPropertyAs...(...)}
340         * methods.
341         * <p>
342         * Every property can be overwritten by a system property prefixed with {@value #SYSTEM_PROPERTY_PREFIX}.
343         * If - for example - the key "updater.force" is to be read and a system property
344         * named "cloudstore.updater.force" is set, this system property is returned instead!
345         * @param key the key identifying the property. Must not be <code>null</code>.
346         * @param defaultValue the default value to fall back to, if neither this {@code Config}'s
347         * internal {@code Properties} nor any of its parents contains a matching entry or
348         * if this entry's value is an empty {@code String}.
349         * May be <code>null</code>.
350         * @return the property's value. Never <code>null</code> unless {@code defaultValue} is <code>null</code>.
351         */
352        public String getPropertyAsNonEmptyTrimmedString(final String key, final String defaultValue) {
353                assertNotNull("key", key);
354                refreshFileHardRefAndCleanOldHardRefs();
355
356                final String sysPropKey = SYSTEM_PROPERTY_PREFIX + key;
357                final String sysPropVal = System.getProperty(sysPropKey);
358                if (sysPropVal != null) {
359                        logger.debug("getPropertyAsNonEmptyTrimmedString: System property with key='{}' and value='{}' overrides config (config is not queried).", sysPropKey, sysPropVal);
360                        return sysPropVal;
361                }
362                logger.debug("getPropertyAsNonEmptyTrimmedString: System property with key='{}' is not set (config is queried next).", sysPropKey);
363
364                synchronized (instanceMutex) {
365                        readIfNeeded();
366                        String sval = properties.getProperty(key);
367                        if (sval == null)
368                                return defaultValue;
369
370                        sval = sval.trim();
371                        if (sval.isEmpty())
372                                return defaultValue;
373
374                        return sval;
375                }
376        }
377
378        public long getPropertyAsLong(final String key, final long defaultValue) {
379                String sval = getPropertyAsNonEmptyTrimmedString(key, null);
380                if (sval == null)
381                        return defaultValue;
382
383                try {
384                        final long lval = Long.parseLong(sval);
385                        return lval;
386                } catch (NumberFormatException x) {
387                        logger.warn("getPropertyAsLong: One of the properties files %s contains the key '%s' (or the system properties override it) with the illegal value '%s'. Falling back to default value '%s'!", propertiesFiles, key, sval, defaultValue);
388                        return defaultValue;
389                }
390        }
391
392        public long getPropertyAsPositiveOrZeroLong(final String key, final long defaultValue) {
393                final long value = getPropertyAsLong(key, defaultValue);
394                if (value < 0) {
395                        logger.warn("getPropertyAsPositiveOrZeroLong: One of the properties files %s contains the key '%s' (or the system properties override it) with the negative value '%s' (only values >= 0 are allowed). Falling back to default value '%s'!", propertiesFiles, key, value, defaultValue);
396                        return defaultValue;
397                }
398                return value;
399        }
400
401        public int getPropertyAsInt(final String key, final int defaultValue) {
402                String sval = getPropertyAsNonEmptyTrimmedString(key, null);
403                if (sval == null)
404                        return defaultValue;
405
406                try {
407                        final int ival = Integer.parseInt(sval);
408                        return ival;
409                } catch (NumberFormatException x) {
410                        logger.warn("getPropertyAsInt: One of the properties files %s contains the key '%s' (or the system properties override it) with the illegal value '%s'. Falling back to default value '%s'!", propertiesFiles, key, sval, defaultValue);
411                        return defaultValue;
412                }
413        }
414
415        public int getPropertyAsPositiveOrZeroInt(final String key, final int defaultValue) {
416                final int value = getPropertyAsInt(key, defaultValue);
417                if (value < 0) {
418                        logger.warn("getPropertyAsPositiveOrZeroInt: One of the properties files %s contains the key '%s' (or the system properties override it) with the negative value '%s' (only values >= 0 are allowed). Falling back to default value '%s'!", propertiesFiles, key, value, defaultValue);
419                        return defaultValue;
420                }
421                return value;
422        }
423
424        /**
425         * Gets the property identified by the given key.
426         * @param key the key identifying the property. Must not be <code>null</code>.
427         * @param defaultValue the default value to fall back to, if neither this {@code Config}'s
428         * internal {@code Properties} nor any of its parents contains a matching entry or
429         * if this entry's value does not match any possible enum value. Must not be <code>null</code>.
430         * If a <code>null</code> default value is required, use {@link #getPropertyAsEnum(String, Class, Enum)}
431         * instead!
432         * @return the property's value. Never <code>null</code>.
433         * @see #getPropertyAsEnum(String, Class, Enum)
434         * @see #getPropertyAsNonEmptyTrimmedString(String, String)
435         */
436        public <E extends Enum<E>> E getPropertyAsEnum(final String key, final E defaultValue) {
437                assertNotNull("defaultValue", defaultValue);
438                @SuppressWarnings("unchecked")
439                Class<E> enumClass = (Class<E>) defaultValue.getClass();
440                return getPropertyAsEnum(key, enumClass, defaultValue);
441        }
442
443        /**
444         * Gets the property identified by the given key.
445         * @param key the key identifying the property. Must not be <code>null</code>.
446         * @param enumClass the enum's type. Must not be <code>null</code>.
447         * @param defaultValue the default value to fall back to, if neither this {@code Config}'s
448         * internal {@code Properties} nor any of its parents contains a matching entry or
449         * if this entry's value does not match any possible enum value. May be <code>null</code>.
450         * @return the property's value. Never <code>null</code> unless {@code defaultValue} is <code>null</code>.
451         * @see #getPropertyAsEnum(String, Enum)
452         * @see #getPropertyAsNonEmptyTrimmedString(String, String)
453         */
454        public <E extends Enum<E>> E getPropertyAsEnum(final String key, final Class<E> enumClass, final E defaultValue) {
455                assertNotNull("enumClass", enumClass);
456                String sval = getPropertyAsNonEmptyTrimmedString(key, null);
457                if (sval == null)
458                        return defaultValue;
459
460                try {
461                        return Enum.valueOf(enumClass, sval);
462                } catch (IllegalArgumentException x) {
463                        logger.warn("getPropertyAsEnum: One of the properties files %s contains the key '%s' with the illegal value '%s'. Falling back to default value '%s'!", propertiesFiles, key, sval, defaultValue);
464                        return defaultValue;
465                }
466        }
467
468        public boolean getPropertyAsBoolean(final String key, final boolean defaultValue) {
469                String sval = getPropertyAsNonEmptyTrimmedString(key, null);
470                if (sval == null)
471                        return defaultValue;
472
473                if (TRUE_STRING.equalsIgnoreCase(sval))
474                        return true;
475                else if (FALSE_STRING.equalsIgnoreCase(sval))
476                        return false;
477                else {
478                        logger.warn("getPropertyAsBoolean: One of the properties files %s contains the key '%s' with the illegal value '%s'. Falling back to default value '%s'!", propertiesFiles, key, sval, defaultValue);
479                        return defaultValue;
480                }
481        }
482
483        private static final void refreshFileHardRefAndCleanOldHardRefs(final Config config) {
484                final File config_file = assertNotNull("config", config).getFile();
485                if (config_file != null)
486                        refreshFileHardRefAndCleanOldHardRefs(config_file);
487        }
488
489        private final void refreshFileHardRefAndCleanOldHardRefs() {
490                if (parentConfig != null)
491                        parentConfig.refreshFileHardRefAndCleanOldHardRefs();
492
493                refreshFileHardRefAndCleanOldHardRefs(this);
494        }
495
496        private static final void refreshFileHardRefAndCleanOldHardRefs(final File config_file) {
497                assertNotNull("config_file", config_file);
498                synchronized (fileHardRefs) {
499                        // make sure the config_file is at the end of fileHardRefs
500                        fileHardRefs.remove(config_file);
501                        fileHardRefs.add(config_file);
502
503                        // remove the first entry until size does not exceed limit anymore.
504                        while (fileHardRefs.size() > fileHardRefsMaxSize)
505                                fileHardRefs.remove(fileHardRefs.iterator().next());
506                }
507        }
508}