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}