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