001package co.codewizards.cloudstore.core.config;
002
003import static co.codewizards.cloudstore.core.io.StreamUtil.*;
004import static co.codewizards.cloudstore.core.oio.OioFileFactory.*;
005import static co.codewizards.cloudstore.core.util.AssertUtil.*;
006import static co.codewizards.cloudstore.core.util.PropertiesUtil.*;
007import static co.codewizards.cloudstore.core.util.StringUtil.*;
008import static java.util.Objects.*;
009
010import java.io.IOException;
011import java.io.InputStream;
012import java.io.OutputStream;
013import java.lang.ref.SoftReference;
014import java.lang.ref.WeakReference;
015import java.util.ArrayList;
016import java.util.Collections;
017import java.util.Date;
018import java.util.HashMap;
019import java.util.Iterator;
020import java.util.LinkedHashSet;
021import java.util.LinkedList;
022import java.util.List;
023import java.util.Map;
024import java.util.Properties;
025import java.util.WeakHashMap;
026import java.util.regex.Matcher;
027import java.util.regex.Pattern;
028
029import org.slf4j.Logger;
030import org.slf4j.LoggerFactory;
031
032import co.codewizards.cloudstore.core.appid.AppIdRegistry;
033import co.codewizards.cloudstore.core.io.LockFile;
034import co.codewizards.cloudstore.core.io.LockFileFactory;
035import co.codewizards.cloudstore.core.oio.File;
036import co.codewizards.cloudstore.core.repo.local.LocalRepoHelper;
037import co.codewizards.cloudstore.core.repo.local.LocalRepoManager;
038import co.codewizards.cloudstore.core.util.ISO8601;
039
040/**
041 * Configuration of CloudStore supporting inheritance of settings.
042 * <p>
043 * See {@link Config}.
044 *
045 * @author Marco หงุ่ยตระกูล-Schulze - marco at codewizards dot co
046 */
047public class ConfigImpl implements Config {
048        private static final Logger logger = LoggerFactory.getLogger(ConfigImpl.class);
049
050        private static final long fileRefsCleanPeriod = 60000L;
051        private static long fileRefsCleanLastTimestamp;
052
053//      private static final String PROPERTIES_FILE_NAME_FOR_DIRECTORY_LOCAL = '.' + APP_ID_SIMPLE_ID + ".local.properties";
054
055//      private static final String PROPERTIES_FILE_NAME_FOR_DIRECTORY = '.' + APP_ID_SIMPLE_ID + ".properties";
056
057        /**
058         * @deprecated We should only support one of these files - this is unnecessary!
059         */
060        @Deprecated
061        private static final String PROPERTIES_FILE_NAME_FOR_DIRECTORY_VISIBLE = APP_ID_SIMPLE_ID + ".properties";
062
063        private static final String PROPERTIES_TEMPLATE_FILE_NAME = "cloudstore.properties"; // *NOT* dependent on AppId!
064
065        private static final String PROPERTIES_FILE_FORMAT_FOR_FILE_HIDDEN = ".%s." + APP_ID_SIMPLE_ID + ".properties";
066
067        /**
068         * @deprecated We should only support one of these files - this is unnecessary!
069         */
070        @Deprecated
071        private static final String PROPERTIES_FILE_FORMAT_FOR_FILE_VISIBLE = "%s." + APP_ID_SIMPLE_ID + ".properties";
072
073        private static final String TRUE_STRING = Boolean.TRUE.toString();
074        private static final String FALSE_STRING = Boolean.FALSE.toString();
075
076        private static final LinkedHashSet<File> fileHardRefs = new LinkedHashSet<>();
077        private static final int fileHardRefsMaxSize = 30;
078        /**
079         * {@link SoftReference}s to the files used in {@link #file2Config}.
080         * <p>
081         * There is no {@code SoftHashMap}, hence we use a WeakHashMap combined with the {@code SoftReference}s here.
082         * @see #file2Config
083         */
084        private static final LinkedList<SoftReference<File>> fileSoftRefs = new LinkedList<>();
085        /**
086         * @see #fileSoftRefs
087         */
088        private static final Map<File, ConfigImpl> file2Config = new WeakHashMap<File, ConfigImpl>();
089
090        private static final class ConfigHolder {
091                public static final ConfigImpl instance = new ConfigImpl(
092                                null, null,
093                                new File[] { createFile(ConfigDir.getInstance().getFile(), PROPERTIES_FILE_NAME_FOR_DIRECTORY_VISIBLE) });
094        }
095
096        private final ConfigImpl parentConfig;
097        private final WeakReference<File> fileRef;
098        protected final File[] propertiesFiles;
099        private final long[] propertiesFilesLastModified;
100        protected final Properties properties;
101
102        private static final Object classMutex = ConfigImpl.class;
103        private final Object instanceMutex;
104
105        private long version = 0;
106
107        protected ConfigImpl(final ConfigImpl parentConfig, final File file, final File [] propertiesFiles) {
108                this.parentConfig = parentConfig;
109
110                if (parentConfig == null)
111                        fileRef = null;
112                else
113                        fileRef = new WeakReference<File>(requireNonNull(file, "file"));
114
115                this.propertiesFiles = assertNotNullAndNoNullElement(propertiesFiles, "propertiesFiles");
116                properties = new Properties(parentConfig == null ? null : parentConfig.properties);
117                propertiesFilesLastModified = new long[propertiesFiles.length];
118                instanceMutex = properties;
119
120                // Create the default global configuration (it's an empty template with some comments).
121                if (parentConfig == null && !propertiesFiles[0].exists()) {
122                        try {
123                                AppIdRegistry.getInstance().copyResourceResolvingAppId(
124                                                ConfigImpl.class, "/" + PROPERTIES_TEMPLATE_FILE_NAME, propertiesFiles[0]);
125                        } catch (final IOException e) {
126                                throw new RuntimeException(e);
127                        }
128                }
129        }
130
131        /**
132         * Get the directory or file for which this Config instance is responsible.
133         * @return the directory or file for which this Config instance is responsible. Might be <code>null</code>, if already
134         * garbage-collected or if this is the root-parent-Config. We try to make garbage-collection extremely unlikely
135         * as long as the Config is held in memory.
136         */
137        protected File getFile() {
138                return fileRef == null ? null : fileRef.get();
139        }
140
141        private static void cleanFileRefs() {
142                synchronized (classMutex) {
143                        if (System.currentTimeMillis() - fileRefsCleanLastTimestamp < fileRefsCleanPeriod)
144                                return;
145
146                        for (final Iterator<SoftReference<File>> it = fileSoftRefs.iterator(); it.hasNext(); ) {
147                                final SoftReference<File> fileRef = it.next();
148                                if (fileRef.get() == null)
149                                        it.remove();
150                        }
151                        fileRefsCleanLastTimestamp = System.currentTimeMillis();
152                }
153        }
154
155        /**
156         * Gets the global {@code Config} for the current user.
157         * @return the global {@code Config} for the current user. Never <code>null</code>.
158         */
159        public static Config getInstance() {
160                return ConfigHolder.instance;
161        }
162
163        /**
164         * Gets the {@code Config} for the given {@code directory}.
165         * @param directory a directory inside a repository. Must not be <code>null</code>.
166         * The directory does not need to exist (it may be created later).
167         * @return the {@code Config} for the given {@code directory}. Never <code>null</code>.
168         */
169        public static Config getInstanceForDirectory(final File directory) {
170                return getInstance(directory, true);
171        }
172
173        /**
174         * Gets the {@code Config} for the given {@code file}.
175         * @param file a file inside a repository. Must not be <code>null</code>.
176         * The file does not need to exist (it may be created later).
177         * @return the {@code Config} for the given {@code file}. Never <code>null</code>.
178         */
179        public static Config getInstanceForFile(final File file) {
180                return getInstance(file, false);
181        }
182
183        private static Config getInstance(final File file, final boolean isDirectory) {
184                requireNonNull(file, "file");
185                cleanFileRefs();
186
187                File config_file = null;
188                ConfigImpl config;
189                synchronized (classMutex) {
190                        config = file2Config.get(file);
191                        if (config != null) {
192                                config_file = config.getFile();
193                                if (config_file == null) // very unlikely, but it actually *can* happen.
194                                        config = null; // we try to make it extremely probable that the Config we return does have a valid file reference.
195                        }
196
197                        if (config == null) {
198                                final File localRoot = LocalRepoHelper.getLocalRootContainingFile(file);
199                                if (localRoot == null)
200                                        throw new IllegalArgumentException("file is not inside a repository: " + file.getAbsolutePath());
201
202                                final ConfigImpl parentConfig = (ConfigImpl) (localRoot == file ? getInstance() : getInstance(file.getParentFile(), true));
203                                config = new ConfigImpl(parentConfig, file, createPropertiesFiles(file, isDirectory));
204                                file2Config.put(file, config);
205                                fileSoftRefs.add(new SoftReference<File>(file));
206                                config_file = config.getFile();
207                        }
208                        requireNonNull(config_file, "config_file");
209                }
210                refreshFileHardRefAndCleanOldHardRefs(config_file);
211                return config;
212        }
213
214        private static File[] createPropertiesFiles(final File file, final boolean isDirectory) {
215                if (isDirectory) {
216                        List<File> files = new ArrayList<>();
217                        File metaDir = createFile(file, LocalRepoManager.META_DIR_NAME);
218                        if (metaDir.isDirectory())
219                                files.add(createFile(metaDir, PROPERTIES_FILE_NAME_PARENT));
220
221                        files.add(createFile(file, PROPERTIES_FILE_NAME_FOR_DIRECTORY));
222                        files.add(createFile(file, PROPERTIES_FILE_NAME_FOR_DIRECTORY_VISIBLE));
223                        files.add(createFile(file, PROPERTIES_FILE_NAME_FOR_DIRECTORY_LOCAL)); // overrides the settings of the shared file!
224                        return files.toArray(new File[files.size()]);
225                }
226                else {
227                        return new File[] {
228                                createFile(file.getParentFile(), String.format(PROPERTIES_FILE_FORMAT_FOR_FILE_HIDDEN, file.getName())),
229                                createFile(file.getParentFile(), String.format(PROPERTIES_FILE_FORMAT_FOR_FILE_VISIBLE, file.getName()))
230                        };
231                }
232        }
233
234        private void readIfNeeded() {
235                synchronized (instanceMutex) {
236                        for (int i = 0; i < propertiesFiles.length; i++) {
237                                final File propertiesFile = propertiesFiles[i];
238                                final long lastModified = propertiesFilesLastModified[i];
239                                if (propertiesFile.lastModified() != lastModified) {
240                                        read();
241                                        break;
242                                }
243                        }
244                }
245
246                if (parentConfig != null)
247                        parentConfig.readIfNeeded();
248        }
249
250        private void read() {
251                synchronized (instanceMutex) {
252                        logger.trace("read: Entered instanceMutex.");
253                        try {
254                                properties.clear();
255                                version = 0;
256                                for (int i = 0; i < propertiesFiles.length; i++) {
257                                        final File propertiesFile = propertiesFiles[i];
258                                        logger.debug("read: Reading propertiesFile '{}'.", propertiesFile.getAbsolutePath());
259                                        final long lastModified = getLastModifiedAndWaitIfNeeded(propertiesFile);
260                                        if (propertiesFile.exists()) { // prevent the properties file from being modified while we're reading it.
261                                                try ( LockFile lockFile = LockFileFactory.getInstance().acquire(propertiesFile, 10000); ) { // TODO maybe system property for timeout?
262                                                        final InputStream in = castStream(lockFile.createInputStream());
263                                                        try {
264                                                                properties.load(in);
265                                                        } finally {
266                                                                in.close();
267                                                        }
268                                                }
269                                        }
270                                        propertiesFilesLastModified[i] = lastModified;
271                                        version += lastModified;
272                                }
273                        } catch (final IOException e) {
274                                properties.clear();
275                                throw new RuntimeException(e);
276                        }
277                }
278        }
279
280        private void write() {
281                synchronized (instanceMutex) {
282                        logger.trace("read: Entered instanceMutex.");
283                        try {
284                                // TODO We should switch to another Properties implementation (our own?! didn't I write one, already? where do I have this code?!)
285                                // Using java.util.Properties causes the entries' order to be randomized and all comments in the file to be lost :-(
286
287                                // Which of the multiple files is used? We overwrite this, if it's only one.
288
289                                File propertiesFile = getSinglePropertiesFile();
290                                if (propertiesFile == null)
291                                        propertiesFile = propertiesFiles[propertiesFiles.length - 1]; // the last one has the last word ;-)
292
293                                logger.debug("write: Writing propertiesFile '{}'.", propertiesFile.getAbsolutePath());
294                                try ( LockFile lockFile = LockFileFactory.getInstance().acquire(propertiesFile, 10000); ) { // TODO maybe system property for timeout?
295                                        final OutputStream out = castStream(lockFile.createOutputStream());
296                                        try {
297                                                properties.store(out, null);
298                                        } finally {
299                                                out.close();
300                                        }
301                                }
302
303                                // TODO should we set propertiesFilesLastModified[...] to prevent re-reading?! would be more efficient - but then, we rarely ever write anyway.
304                        } catch (final IOException e) {
305                                properties.clear();
306                                throw new RuntimeException(e);
307                        }
308                }
309        }
310
311        private File getSinglePropertiesFile() {
312                File result = null;
313                for (final File propertiesFile : propertiesFiles) {
314                        if (propertiesFile.exists()) {
315                                if (result == null)
316                                        result = propertiesFile;
317                                else
318                                        return null; // multiple in use
319                        }
320                }
321
322//              if (result == null) // none in use, yet => choose the .* one (the first)
323//                      result = propertiesFiles[0]; // now using the local file by default (the last)
324
325                return result;
326        }
327
328        /**
329         * Gets the {@link File#lastModified() lastModified} timestamp of the given {@code file}
330         * and waits if needed.
331         * <p>
332         * Waiting is needed, if the modification's age is shorter than the file system's time granularity.
333         * Since we do not know the file system's time granularity, we assume 2 seconds. Thus, if the file
334         * was changed e.g. 600 ms before invoking this method, the method will wait for 1400 ms to make sure
335         * the modification is at least as old as the assumed file system's temporal granularity.
336         * <p>
337         * This waiting strategy makes sure that a future modification of the file, after the file was read,
338         * is reliably detected - causing the file to be read again.
339         * @param file the file whose {@link File#lastModified() lastModified} timestamp to obtain. Must not be <code>null</code>.
340         * @return the {@link File#lastModified() lastModified} timestamp. 0, if the specified {@code file}
341         * does not exist.
342         */
343        private long getLastModifiedAndWaitIfNeeded(final File file) {
344                requireNonNull(file, "file");
345                long lastModified = file.lastModified(); // is 0 for non-existing file
346                final long now = System.currentTimeMillis();
347
348                // Check and handle timestamp in the future.
349                if (lastModified > now) {
350                        file.setLastModified(now);
351                        logger.warn("getLastModifiedAndWaitIfNeeded: lastModified of '{}' was in the future! Changed it to now!", file.getAbsolutePath());
352
353                        lastModified = file.lastModified();
354                        if (lastModified > now) {
355                                logger.error("getLastModifiedAndWaitIfNeeded: lastModified of '{}' is in the future! Changing it FAILED! Permissions?!", file.getAbsolutePath());
356                                return lastModified;
357                        }
358                }
359
360                // Wait, if the modification is not yet older than the file system's (assumed!) granularity.
361                // No file system should have a granularity worse than 2 seconds. Waiting max. 2 seconds in this use-case
362                // in this rare situation is acceptable. After all, this is a config file which isn't changed often.
363                final long fileSystemTemporalGranularity = 2000; // TODO maybe make this configurable?! Warning: we are in the config here - accessing the config is thus not so easy (=> recursion).
364                final long modificationAge = now - lastModified;
365                final long waitPeriod = fileSystemTemporalGranularity - modificationAge;
366                if (waitPeriod > 0) {
367                        logger.info("getLastModifiedAndWaitIfNeeded: Waiting {} ms.", waitPeriod);
368                        try { Thread.sleep(waitPeriod); } catch (InterruptedException e) { }
369                }
370
371                return lastModified;
372        }
373
374        @Override
375        public long getVersion() {
376                long result;
377
378                synchronized (instanceMutex) {
379                        readIfNeeded();
380                        result = version;
381                }
382
383                if (parentConfig != null)
384                        result += parentConfig.getVersion();
385
386                return result;
387        }
388
389        @Override
390        public String getProperty(final String key, final String defaultValue) {
391                requireNonNull(key, "key");
392                refreshFileHardRefAndCleanOldHardRefs();
393
394                final String sysPropKey = SYSTEM_PROPERTY_PREFIX + key;
395                final String sysPropVal = System.getProperty(sysPropKey);
396                if (sysPropVal != null) {
397                        logger.debug("getProperty: System property with key='{}' and value='{}' overrides config (config is not queried).", sysPropKey, sysPropVal);
398                        return sysPropVal;
399                }
400
401                final String envVarKey = systemPropertyToEnvironmentVariable(sysPropKey);
402                final String envVarVal = System.getenv(envVarKey);
403                if (envVarVal != null) {
404                        logger.debug("getProperty: Environment variable with key='{}' and value='{}' overrides config (config is not queried).", envVarKey, envVarVal);
405                        return envVarVal;
406                }
407
408                logger.debug("getProperty: System property with key='{}' is not set (config is queried next).", sysPropKey);
409
410                synchronized (instanceMutex) {
411                        readIfNeeded();
412                        return properties.getProperty(key, defaultValue);
413                }
414        }
415
416        @Override
417        public String getDirectProperty(final String key) {
418                requireNonNull(key, "key");
419
420                // TODO should we really take system properties and environment variables into account?!
421
422                final String sysPropKey = SYSTEM_PROPERTY_PREFIX + key;
423                final String sysPropVal = System.getProperty(sysPropKey);
424                if (sysPropVal != null) {
425                        logger.debug("getProperty: System property with key='{}' and value='{}' overrides config (config is not queried).", sysPropKey, sysPropVal);
426                        return sysPropVal;
427                }
428
429                final String envVarKey = systemPropertyToEnvironmentVariable(sysPropKey);
430                final String envVarVal = System.getenv(envVarKey);
431                if (envVarVal != null) {
432                        logger.debug("getProperty: Environment variable with key='{}' and value='{}' overrides config (config is not queried).", envVarKey, envVarVal);
433                        return envVarVal;
434                }
435
436                refreshFileHardRefAndCleanOldHardRefs();
437                synchronized (instanceMutex) {
438                        readIfNeeded();
439                        return (String) properties.get(key);
440                }
441        }
442
443        @Override
444        public void setDirectProperty(final String key, final String value) {
445                requireNonNull(key, "key");
446
447                // TODO really prevent modifying values? Or handle system props + env-vars differently?
448
449                final String sysPropKey = SYSTEM_PROPERTY_PREFIX + key;
450                if (System.getProperty(sysPropKey) != null) {
451                        throw new IllegalStateException(String.format(
452                                        "System property with key='%s' overrides config. The property '%s' can therefore not be modified.", sysPropKey, key));
453                }
454
455                final String envVarKey = systemPropertyToEnvironmentVariable(sysPropKey);
456                if (System.getenv(envVarKey) != null) {
457                        throw new IllegalStateException(String.format(
458                                        "Environment variable with key='%s' overrides config. The property '%s' can therefore not be modified.", envVarKey, key));
459                }
460
461                refreshFileHardRefAndCleanOldHardRefs();
462                synchronized (instanceMutex) {
463                        readIfNeeded();
464                        if (value == null)
465                                properties.remove(key);
466                        else
467                                properties.put(key, value);
468
469                        write();
470                }
471        }
472
473        @Override
474        public String getPropertyAsNonEmptyTrimmedString(final String key, final String defaultValue) {
475                requireNonNull(key, "key");
476                refreshFileHardRefAndCleanOldHardRefs();
477
478                final String sysPropKey = SYSTEM_PROPERTY_PREFIX + key;
479                final String sysPropVal = trim(System.getProperty(sysPropKey));
480                if (! isEmpty(sysPropVal)) {
481                        logger.debug("getPropertyAsNonEmptyTrimmedString: System property with key='{}' and value='{}' overrides config (config is not queried).", sysPropKey, sysPropVal);
482                        return sysPropVal;
483                }
484
485                final String envVarKey = systemPropertyToEnvironmentVariable(sysPropKey);
486                final String envVarVal = trim(System.getenv(envVarKey));
487                if (! isEmpty(envVarVal)) {
488                        logger.debug("getPropertyAsNonEmptyTrimmedString: Environment variable with key='{}' and value='{}' overrides config (config is not queried).", envVarKey, envVarVal);
489                        return envVarVal;
490                }
491
492                logger.debug("getPropertyAsNonEmptyTrimmedString: System property with key='{}' is not set (config is queried next).", sysPropKey);
493
494                synchronized (instanceMutex) {
495                        readIfNeeded();
496                        String sval = trim(properties.getProperty(key));
497                        if (isEmpty(sval))
498                                return defaultValue;
499
500                        return sval;
501                }
502        }
503
504        @Override
505        public long getPropertyAsLong(final String key, final long defaultValue) {
506                final String sval = getPropertyAsNonEmptyTrimmedString(key, null);
507                if (sval == null)
508                        return defaultValue;
509
510                try {
511                        final long lval = Long.parseLong(sval);
512                        return lval;
513                } catch (final NumberFormatException x) {
514                        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);
515                        return defaultValue;
516                }
517        }
518
519        @Override
520        public long getPropertyAsPositiveOrZeroLong(final String key, final long defaultValue) {
521                final long value = getPropertyAsLong(key, defaultValue);
522                if (value < 0) {
523                        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);
524                        return defaultValue;
525                }
526                return value;
527        }
528
529        @Override
530        public int getPropertyAsInt(final String key, final int defaultValue) {
531                final String sval = getPropertyAsNonEmptyTrimmedString(key, null);
532                if (sval == null)
533                        return defaultValue;
534
535                try {
536                        final int ival = Integer.parseInt(sval);
537                        return ival;
538                } catch (final NumberFormatException x) {
539                        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);
540                        return defaultValue;
541                }
542        }
543
544        @Override
545        public int getPropertyAsPositiveOrZeroInt(final String key, final int defaultValue) {
546                final int value = getPropertyAsInt(key, defaultValue);
547                if (value < 0) {
548                        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);
549                        return defaultValue;
550                }
551                return value;
552        }
553
554        @Override
555        public <E extends Enum<E>> E getPropertyAsEnum(final String key, final E defaultValue) {
556                requireNonNull(defaultValue, "defaultValue");
557                @SuppressWarnings("unchecked")
558                final Class<E> enumClass = (Class<E>) defaultValue.getClass();
559                return getPropertyAsEnum(key, enumClass, defaultValue);
560        }
561
562        @Override
563        public <E extends Enum<E>> E getPropertyAsEnum(final String key, final Class<E> enumClass, final E defaultValue) {
564                requireNonNull(enumClass, "enumClass");
565                final String sval = getPropertyAsNonEmptyTrimmedString(key, null);
566                if (sval == null)
567                        return defaultValue;
568
569                try {
570                        return Enum.valueOf(enumClass, sval);
571                } catch (final IllegalArgumentException x) {
572                        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);
573                        return defaultValue;
574                }
575        }
576
577        @Override
578        public boolean getPropertyAsBoolean(final String key, final boolean defaultValue) {
579                final String sval = getPropertyAsNonEmptyTrimmedString(key, null);
580                if (sval == null)
581                        return defaultValue;
582
583                if (TRUE_STRING.equalsIgnoreCase(sval))
584                        return true;
585                else if (FALSE_STRING.equalsIgnoreCase(sval))
586                        return false;
587                else {
588                        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);
589                        return defaultValue;
590                }
591        }
592
593        @Override
594        public Date getPropertyAsDate(final String key, final Date defaultValue) {
595                final String sval = getPropertyAsNonEmptyTrimmedString(key, null);
596                if (sval == null)
597                        return defaultValue;
598
599                Date date = ISO8601.parseDate(sval);
600                if (date == null) {
601                        logger.warn("getPropertyAsDate: 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);
602                        return defaultValue;
603                }
604                return date;
605        }
606
607        private static final void refreshFileHardRefAndCleanOldHardRefs(final ConfigImpl config) {
608                final File config_file = requireNonNull(config, "config").getFile();
609                if (config_file != null)
610                        refreshFileHardRefAndCleanOldHardRefs(config_file);
611        }
612
613        private final void refreshFileHardRefAndCleanOldHardRefs() {
614                if (parentConfig != null)
615                        parentConfig.refreshFileHardRefAndCleanOldHardRefs();
616
617                refreshFileHardRefAndCleanOldHardRefs(this);
618        }
619
620        private static final void refreshFileHardRefAndCleanOldHardRefs(final File config_file) {
621                requireNonNull(config_file, "config_file");
622                synchronized (fileHardRefs) {
623                        // make sure the config_file is at the end of fileHardRefs
624                        fileHardRefs.remove(config_file);
625                        fileHardRefs.add(config_file);
626
627                        // remove the first entry until size does not exceed limit anymore.
628                        while (fileHardRefs.size() > fileHardRefsMaxSize)
629                                fileHardRefs.remove(fileHardRefs.iterator().next());
630                }
631        }
632
633        @Override
634        public Map<String, List<String>> getKey2GroupsMatching(final Pattern regex) {
635                requireNonNull(regex, "regex");
636                refreshFileHardRefAndCleanOldHardRefs();
637
638                final Map<String, List<String>> key2Groups = new HashMap<>();
639                populateKeysMatching(key2Groups, regex);
640                return Collections.unmodifiableMap(key2Groups);
641        }
642
643        protected void populateKeysMatching(final Map<String, List<String>> key2Groups, final Pattern regex) {
644                requireNonNull(key2Groups, "key2Groups");
645                requireNonNull(regex, "regex");
646                if (parentConfig != null)
647                        parentConfig.populateKeysMatching(key2Groups, regex);
648
649                synchronized (instanceMutex) {
650                        readIfNeeded();
651
652                        for (final Object k : properties.keySet()) {
653                                final String key = (String) k;
654                                if (key2Groups.containsKey(key))
655                                        continue;
656
657                                final Matcher matcher = regex.matcher(key);
658                                if (matcher.matches()) {
659                                        final int groupCount = matcher.groupCount();
660                                        final List<String> groups = new ArrayList<>(groupCount);
661                                        for (int i = 1; i <= groupCount; ++i) // ignore group 0, because this is the same as key.
662                                                groups.add(matcher.group(i));
663
664                                        key2Groups.put(key, Collections.unmodifiableList(groups));
665                                }
666                        }
667                }
668        }
669}