001package co.codewizards.cloudstore.core.config; 002 003import static co.codewizards.cloudstore.core.util.Util.*; 004 005import java.io.File; 006import java.io.IOException; 007import java.io.InputStream; 008import java.lang.ref.SoftReference; 009import java.lang.ref.WeakReference; 010import java.util.Iterator; 011import java.util.LinkedHashSet; 012import java.util.LinkedList; 013import java.util.Map; 014import java.util.Properties; 015import java.util.WeakHashMap; 016 017import org.slf4j.Logger; 018import org.slf4j.LoggerFactory; 019 020import co.codewizards.cloudstore.core.io.LockFile; 021import co.codewizards.cloudstore.core.io.LockFileFactory; 022import co.codewizards.cloudstore.core.repo.local.LocalRepoHelper; 023import co.codewizards.cloudstore.core.util.IOUtil; 024 025/** 026 * Configuration of CloudStore supporting inheritance of settings. 027 * <p> 028 * There is one {@code Config} instance available (lazily created, cached temporarily) for every 029 * directory and every file in a repository. Each {@code Config} inherits the settings from the 030 * parent-directory, if not explicitly overwritten. 031 * <p> 032 * The configuration is based on {@link Properties} files. Every property file is optional. If it 033 * does not exist, all settings are inherited. If it does exist, only those properties contained in 034 * the file are overriden. All properties not contained in the file are still inherited. Inheritance 035 * is thus applicable on every individual property. 036 * <p> 037 * Modifications, deletions, creations of properties files are detected during runtime (pretty immediately). 038 * Note, that this detection is based on the files' timestamps. Since most file systems have a granularity 039 * of 1 second (some even 2) for the last-modified-timestamp, multiple modifications in the same second might 040 * not be detected. 041 * <p> 042 * There is a global properties file in the user's home directory (or wherever {@link ConfigDir} 043 * points to): <code>/home/tomcat/.cloudstore/cloudstore.properties</code> 044 * <p> 045 * Additionally, every directory can optionally contain the following files: 046 * <ol> 047 * <li><code>.cloudstore.properties</code> 048 * <li><code>cloudstore.properties</code> 049 * <li><code>.${anyFileName}.cloudstore.properties</code> 050 * <li><code>${anyFileName}.cloudstore.properties</code> 051 * </ol> 052 * <p> 053 * The files 1. and 2. are applicable to the entire directory and all sub-directories and files in it. 054 * Usually, on GNU/Linux people will prefer 1., but when using Windows, files starting with a "." are 055 * sometimes a bit hard to deal with. Therefore, we support both. The file 2. overrides the settings of file 1.. 056 * <p> 057 * The files 3. and 4. are applicable only to the file <code>${anyFileName}</code>. Thus, if you want 058 * to set special behaviour for the file <code>example.db</code> only, you can create the file 059 * <code>.example.db.cloudstore.properties</code> in the same directory. 060 * 061 * @author Marco หงุ่ยตระกูล-Schulze - marco at codewizards dot co 062 */ 063public class Config { 064 private static final Logger logger = LoggerFactory.getLogger(Config.class); 065 066 private static final long fileRefsCleanPeriod = 60000L; 067 private static long fileRefsCleanLastTimestamp; 068 069 private static final String PROPERTIES_FILE_NAME_FOR_DIRECTORY_HIDDEN = ".cloudstore.properties"; 070 private static final String PROPERTIES_FILE_NAME_FOR_DIRECTORY_VISIBLE = "cloudstore.properties"; 071 072 private static final String PROPERTIES_FILE_FORMAT_FOR_FILE_HIDDEN = ".%s.cloudstore.properties"; 073 private static final String PROPERTIES_FILE_FORMAT_FOR_FILE_VISIBLE = "%s.cloudstore.properties"; 074 075 private static final String TRUE_STRING = Boolean.TRUE.toString(); 076 private static final String FALSE_STRING = Boolean.FALSE.toString(); 077 078 /** 079 * Prefix used for system properties overriding configuration entries. 080 * <p> 081 * Every property in the configuration (i.e. in its properties files) can be overridden 082 * by a corresponding system property. The system property must be prefixed. 083 * <p> 084 * For example, to override the configuration property with the key "deferrableExecutor.timeout", 085 * you can pass the system property "cloudstore.deferrableExecutor.timeout" to the JVM. If the 086 * system property exists, the configuration is not consulted, but the system property value is 087 * used as shortcut. 088 */ 089 public static final String SYSTEM_PROPERTY_PREFIX = "cloudstore."; 090 091 private static final LinkedHashSet<File> fileHardRefs = new LinkedHashSet<>(); 092 private static final int fileHardRefsMaxSize = 30; 093 /** 094 * {@link SoftReference}s to the files used in {@link #file2Config}. 095 * <p> 096 * There is no {@code SoftHashMap}, hence we use a WeakHashMap combined with the {@code SoftReference}s here. 097 * @see #file2Config 098 */ 099 private static final LinkedList<SoftReference<File>> fileSoftRefs = new LinkedList<>(); 100 /** 101 * @see #fileSoftRefs 102 */ 103 private static final Map<File, Config> file2Config = new WeakHashMap<File, Config>(); 104 105 private static final class ConfigHolder { 106 public static final Config instance = new Config( 107 null, null, 108 new File[] { new File(ConfigDir.getInstance().getFile(), PROPERTIES_FILE_NAME_FOR_DIRECTORY_VISIBLE) }); 109 } 110 111 private final Config parentConfig; 112 private final WeakReference<File> fileRef; 113 protected final File[] propertiesFiles; 114 private final long[] propertiesFilesLastModified; 115 private final Properties properties; 116 117 private static final Object classMutex = Config.class; 118 private final Object instanceMutex; 119 120 private Config(Config parentConfig, File file, File [] propertiesFiles) { 121 this.parentConfig = parentConfig; 122 123 if (parentConfig == null) 124 fileRef = null; 125 else 126 fileRef = new WeakReference<File>(assertNotNull("file", file)); 127 128 this.propertiesFiles = assertNotNullAndNoNullElement("propertiesFiles", propertiesFiles); 129 properties = new Properties(parentConfig == null ? null : parentConfig.properties); 130 propertiesFilesLastModified = new long[propertiesFiles.length]; 131 instanceMutex = properties; 132 133 // Create the default global configuration (it's an empty template with some comments). 134 if (parentConfig == null && !propertiesFiles[0].exists()) { 135 try { 136 IOUtil.copyResource(Config.class, "/" + PROPERTIES_FILE_NAME_FOR_DIRECTORY_VISIBLE, propertiesFiles[0]); 137 } catch (IOException e) { 138 throw new RuntimeException(e); 139 } 140 } 141 } 142 143 /** 144 * Get the directory or file for which this Config instance is responsible. 145 * @return the directory or file for which this Config instance is responsible. Might be <code>null</code>, if already 146 * garbage-collected or if this is the root-parent-Config. We try to make garbage-collection extremely unlikely 147 * as long as the Config is held in memory. 148 */ 149 protected File getFile() { 150 return fileRef == null ? null : fileRef.get(); 151 } 152 153 private static void cleanFileRefs() { 154 synchronized (classMutex) { 155 if (System.currentTimeMillis() - fileRefsCleanLastTimestamp < fileRefsCleanPeriod) 156 return; 157 158 for (Iterator<SoftReference<File>> it = fileSoftRefs.iterator(); it.hasNext(); ) { 159 SoftReference<File> fileRef = it.next(); 160 if (fileRef.get() == null) 161 it.remove(); 162 } 163 fileRefsCleanLastTimestamp = System.currentTimeMillis(); 164 } 165 } 166 167 /** 168 * Gets the global {@code Config} for the current user. 169 * @return the global {@code Config} for the current user. Never <code>null</code>. 170 */ 171 public static Config getInstance() { 172 return ConfigHolder.instance; 173 } 174 175 /** 176 * Gets the {@code Config} for the given {@code directory}. 177 * @param directory a directory inside a repository. Must not be <code>null</code>. 178 * The directory does not need to exist (it may be created later). 179 * @return the {@code Config} for the given {@code directory}. Never <code>null</code>. 180 */ 181 public static Config getInstanceForDirectory(final File directory) { 182 return getInstance(directory, true); 183 } 184 185 /** 186 * Gets the {@code Config} for the given {@code file}. 187 * @param file a file inside a repository. Must not be <code>null</code>. 188 * The file does not need to exist (it may be created later). 189 * @return the {@code Config} for the given {@code file}. Never <code>null</code>. 190 */ 191 public static Config getInstanceForFile(final File file) { 192 return getInstance(file, false); 193 } 194 195 private static Config getInstance(final File file, final boolean isDirectory) { 196 assertNotNull("file", file); 197 cleanFileRefs(); 198 199 File config_file = null; 200 Config config; 201 synchronized (classMutex) { 202 config = file2Config.get(file); 203 if (config != null) { 204 config_file = config.getFile(); 205 if (config_file == null) // very unlikely, but it actually *can* happen. 206 config = null; // we try to make it extremely probable that the Config we return does have a valid file reference. 207 } 208 209 if (config == null) { 210 final File localRoot = LocalRepoHelper.getLocalRootContainingFile(file); 211 if (localRoot == null) 212 throw new IllegalArgumentException("file is not inside a repository: " + file.getAbsolutePath()); 213 214 final Config parentConfig = localRoot == file ? getInstance() : getInstance(file.getParentFile(), true); 215 config = new Config(parentConfig, file, createPropertiesFiles(file, isDirectory)); 216 file2Config.put(file, config); 217 fileSoftRefs.add(new SoftReference<File>(file)); 218 config_file = config.getFile(); 219 } 220 assertNotNull("config_file", config_file); 221 } 222 refreshFileHardRefAndCleanOldHardRefs(config_file); 223 return config; 224 } 225 226 private static File[] createPropertiesFiles(final File file, final boolean isDirectory) { 227 if (isDirectory) { 228 return new File[] { 229 new File(file, PROPERTIES_FILE_NAME_FOR_DIRECTORY_HIDDEN), 230 new File(file, PROPERTIES_FILE_NAME_FOR_DIRECTORY_VISIBLE) 231 }; 232 } 233 else { 234 return new File[] { 235 new File(file.getParentFile(), String.format(PROPERTIES_FILE_FORMAT_FOR_FILE_HIDDEN, file.getName())), 236 new File(file.getParentFile(), String.format(PROPERTIES_FILE_FORMAT_FOR_FILE_VISIBLE, file.getName())) 237 }; 238 } 239 } 240 241 private Config readIfNeeded() { 242 synchronized (instanceMutex) { 243 for (int i = 0; i < propertiesFiles.length; i++) { 244 final File propertiesFile = propertiesFiles[i]; 245 final long lastModified = propertiesFilesLastModified[i]; 246 if (propertiesFile.lastModified() != lastModified) { 247 read(); 248 break; 249 } 250 } 251 } 252 253 if (parentConfig != null) 254 parentConfig.readIfNeeded(); 255 256 return this; 257 } 258 259 private void read() { 260 synchronized (instanceMutex) { 261 logger.trace("read: Entered instanceMutex."); 262 try { 263 properties.clear(); 264 for (int i = 0; i < propertiesFiles.length; i++) { 265 final File propertiesFile = propertiesFiles[i]; 266 logger.debug("read: Reading propertiesFile '{}'.", propertiesFile.getAbsolutePath()); 267 final long lastModified = propertiesFile.lastModified(); // is 0 for non-existing file 268 if (propertiesFile.exists()) { // prevent the properties file from being modified while we're reading it. 269 LockFile lockFile = LockFileFactory.getInstance().acquire(propertiesFile, 10000); // TODO maybe system property for timeout? 270 try { 271 final InputStream in = lockFile.createInputStream(); 272 try { 273 properties.load(in); 274 } finally { 275 in.close(); 276 } 277 } finally { 278 lockFile.release(); 279 } 280 } 281 propertiesFilesLastModified[i] = lastModified; 282 } 283 } catch (IOException e) { 284 properties.clear(); 285 throw new RuntimeException(e); 286 } 287 } 288 } 289 290 /** 291 * Gets the property identified by the given key. 292 * <p> 293 * This method directly delegates to {@link Properties#getProperty(String, String)}. 294 * Thus, an empty String in the internal {@code Properties} is returned instead of the 295 * given {@code defaultValue}. The {@code defaultValue} is only returned, if neither 296 * the internal {@code Properties} of this {@code Config} nor any of its parents contains 297 * the entry. 298 * <p> 299 * <b>Important:</b> This is often not the desired behaviour. You might want to use 300 * {@link #getPropertyAsNonEmptyTrimmedString(String, String)} instead! 301 * <p> 302 * Every property can be overwritten by a system property prefixed with {@value #SYSTEM_PROPERTY_PREFIX}. 303 * If - for example - the key "updater.force" is to be read and a system property 304 * named "cloudstore.updater.force" is set, this system property is returned instead! 305 * @param key the key identifying the property. Must not be <code>null</code>. 306 * @param defaultValue the default value to fall back to, if neither this {@code Config}'s 307 * internal {@code Properties} nor any of its parents contains a matching entry. 308 * May be <code>null</code>. 309 * @return the property's value. Never <code>null</code> unless {@code defaultValue} is <code>null</code>. 310 * @see #getPropertyAsNonEmptyTrimmedString(String, String) 311 */ 312 public String getProperty(final String key, final String defaultValue) { 313 assertNotNull("key", key); 314 refreshFileHardRefAndCleanOldHardRefs(); 315 316 final String sysPropKey = SYSTEM_PROPERTY_PREFIX + key; 317 final String sysPropVal = System.getProperty(sysPropKey); 318 if (sysPropVal != null) { 319 logger.debug("getProperty: System property with key='{}' and value='{}' overrides config (config is not queried).", sysPropKey, sysPropVal); 320 return sysPropVal; 321 } 322 logger.debug("getProperty: System property with key='{}' is not set (config is queried next).", sysPropKey); 323 324 synchronized (instanceMutex) { 325 readIfNeeded(); 326 return properties.getProperty(key, defaultValue); 327 } 328 } 329 330 /** 331 * Gets the property identified by the given key; {@linkplain String#trim() trimmed}. 332 * <p> 333 * In contrast to {@link #getProperty(String, String)}, this method falls back to the given 334 * {@code defaultValue}, if the internal {@code Properties} contains an empty {@code String} 335 * (after trimming) as value for the given {@code key}. 336 * <p> 337 * It therefore means that a value set to an empty {@code String} in the properties file means 338 * to use the program's default instead. It is therefore consistent with 339 * {@link #getPropertyAsLong(String, long)} and all other {@code getPropertyAs...(...)} 340 * methods. 341 * <p> 342 * Every property can be overwritten by a system property prefixed with {@value #SYSTEM_PROPERTY_PREFIX}. 343 * If - for example - the key "updater.force" is to be read and a system property 344 * named "cloudstore.updater.force" is set, this system property is returned instead! 345 * @param key the key identifying the property. Must not be <code>null</code>. 346 * @param defaultValue the default value to fall back to, if neither this {@code Config}'s 347 * internal {@code Properties} nor any of its parents contains a matching entry or 348 * if this entry's value is an empty {@code String}. 349 * May be <code>null</code>. 350 * @return the property's value. Never <code>null</code> unless {@code defaultValue} is <code>null</code>. 351 */ 352 public String getPropertyAsNonEmptyTrimmedString(final String key, final String defaultValue) { 353 assertNotNull("key", key); 354 refreshFileHardRefAndCleanOldHardRefs(); 355 356 final String sysPropKey = SYSTEM_PROPERTY_PREFIX + key; 357 final String sysPropVal = System.getProperty(sysPropKey); 358 if (sysPropVal != null) { 359 logger.debug("getPropertyAsNonEmptyTrimmedString: System property with key='{}' and value='{}' overrides config (config is not queried).", sysPropKey, sysPropVal); 360 return sysPropVal; 361 } 362 logger.debug("getPropertyAsNonEmptyTrimmedString: System property with key='{}' is not set (config is queried next).", sysPropKey); 363 364 synchronized (instanceMutex) { 365 readIfNeeded(); 366 String sval = properties.getProperty(key); 367 if (sval == null) 368 return defaultValue; 369 370 sval = sval.trim(); 371 if (sval.isEmpty()) 372 return defaultValue; 373 374 return sval; 375 } 376 } 377 378 public long getPropertyAsLong(final String key, final long defaultValue) { 379 String sval = getPropertyAsNonEmptyTrimmedString(key, null); 380 if (sval == null) 381 return defaultValue; 382 383 try { 384 final long lval = Long.parseLong(sval); 385 return lval; 386 } catch (NumberFormatException x) { 387 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); 388 return defaultValue; 389 } 390 } 391 392 public long getPropertyAsPositiveOrZeroLong(final String key, final long defaultValue) { 393 final long value = getPropertyAsLong(key, defaultValue); 394 if (value < 0) { 395 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); 396 return defaultValue; 397 } 398 return value; 399 } 400 401 public int getPropertyAsInt(final String key, final int defaultValue) { 402 String sval = getPropertyAsNonEmptyTrimmedString(key, null); 403 if (sval == null) 404 return defaultValue; 405 406 try { 407 final int ival = Integer.parseInt(sval); 408 return ival; 409 } catch (NumberFormatException x) { 410 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); 411 return defaultValue; 412 } 413 } 414 415 public int getPropertyAsPositiveOrZeroInt(final String key, final int defaultValue) { 416 final int value = getPropertyAsInt(key, defaultValue); 417 if (value < 0) { 418 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); 419 return defaultValue; 420 } 421 return value; 422 } 423 424 /** 425 * Gets the property identified by the given key. 426 * @param key the key identifying the property. Must not be <code>null</code>. 427 * @param defaultValue the default value to fall back to, if neither this {@code Config}'s 428 * internal {@code Properties} nor any of its parents contains a matching entry or 429 * if this entry's value does not match any possible enum value. Must not be <code>null</code>. 430 * If a <code>null</code> default value is required, use {@link #getPropertyAsEnum(String, Class, Enum)} 431 * instead! 432 * @return the property's value. Never <code>null</code>. 433 * @see #getPropertyAsEnum(String, Class, Enum) 434 * @see #getPropertyAsNonEmptyTrimmedString(String, String) 435 */ 436 public <E extends Enum<E>> E getPropertyAsEnum(final String key, final E defaultValue) { 437 assertNotNull("defaultValue", defaultValue); 438 @SuppressWarnings("unchecked") 439 Class<E> enumClass = (Class<E>) defaultValue.getClass(); 440 return getPropertyAsEnum(key, enumClass, defaultValue); 441 } 442 443 /** 444 * Gets the property identified by the given key. 445 * @param key the key identifying the property. Must not be <code>null</code>. 446 * @param enumClass the enum's type. Must not be <code>null</code>. 447 * @param defaultValue the default value to fall back to, if neither this {@code Config}'s 448 * internal {@code Properties} nor any of its parents contains a matching entry or 449 * if this entry's value does not match any possible enum value. May be <code>null</code>. 450 * @return the property's value. Never <code>null</code> unless {@code defaultValue} is <code>null</code>. 451 * @see #getPropertyAsEnum(String, Enum) 452 * @see #getPropertyAsNonEmptyTrimmedString(String, String) 453 */ 454 public <E extends Enum<E>> E getPropertyAsEnum(final String key, final Class<E> enumClass, final E defaultValue) { 455 assertNotNull("enumClass", enumClass); 456 String sval = getPropertyAsNonEmptyTrimmedString(key, null); 457 if (sval == null) 458 return defaultValue; 459 460 try { 461 return Enum.valueOf(enumClass, sval); 462 } catch (IllegalArgumentException x) { 463 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); 464 return defaultValue; 465 } 466 } 467 468 public boolean getPropertyAsBoolean(final String key, final boolean defaultValue) { 469 String sval = getPropertyAsNonEmptyTrimmedString(key, null); 470 if (sval == null) 471 return defaultValue; 472 473 if (TRUE_STRING.equalsIgnoreCase(sval)) 474 return true; 475 else if (FALSE_STRING.equalsIgnoreCase(sval)) 476 return false; 477 else { 478 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); 479 return defaultValue; 480 } 481 } 482 483 private static final void refreshFileHardRefAndCleanOldHardRefs(final Config config) { 484 final File config_file = assertNotNull("config", config).getFile(); 485 if (config_file != null) 486 refreshFileHardRefAndCleanOldHardRefs(config_file); 487 } 488 489 private final void refreshFileHardRefAndCleanOldHardRefs() { 490 if (parentConfig != null) 491 parentConfig.refreshFileHardRefAndCleanOldHardRefs(); 492 493 refreshFileHardRefAndCleanOldHardRefs(this); 494 } 495 496 private static final void refreshFileHardRefAndCleanOldHardRefs(final File config_file) { 497 assertNotNull("config_file", config_file); 498 synchronized (fileHardRefs) { 499 // make sure the config_file is at the end of fileHardRefs 500 fileHardRefs.remove(config_file); 501 fileHardRefs.add(config_file); 502 503 // remove the first entry until size does not exceed limit anymore. 504 while (fileHardRefs.size() > fileHardRefsMaxSize) 505 fileHardRefs.remove(fileHardRefs.iterator().next()); 506 } 507 } 508}