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