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