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}