001// *************************************************************************************************************************** 002// * Licensed to the Apache Software Foundation (ASF) under one or more contributor license agreements. See the NOTICE file * 003// * distributed with this work for additional information regarding copyright ownership. The ASF licenses this file * 004// * to you under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance * 005// * with the License. You may obtain a copy of the License at * 006// * * 007// * http://www.apache.org/licenses/LICENSE-2.0 * 008// * * 009// * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an * 010// * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the * 011// * specific language governing permissions and limitations under the License. * 012// *************************************************************************************************************************** 013package org.apache.juneau.config.store; 014 015import static java.nio.file.StandardWatchEventKinds.*; 016import static java.nio.file.StandardOpenOption.*; 017import static org.apache.juneau.internal.StringUtils.*; 018 019import java.io.*; 020import java.nio.*; 021import java.nio.channels.*; 022import java.nio.charset.*; 023import java.nio.file.*; 024import java.util.concurrent.*; 025 026import org.apache.juneau.*; 027import org.apache.juneau.annotation.*; 028import org.apache.juneau.collections.*; 029import org.apache.juneau.internal.*; 030 031/** 032 * Filesystem-based storage location for configuration files. 033 * 034 * <p> 035 * Points to a file system directory containing configuration files. 036 */ 037@ConfigurableContext 038public class ConfigFileStore extends ConfigStore { 039 040 //------------------------------------------------------------------------------------------------------------------- 041 // Configurable properties 042 //------------------------------------------------------------------------------------------------------------------- 043 044 static final String PREFIX = "ConfigFileStore"; 045 046 /** 047 * Configuration property: Local file system directory. 048 * 049 * <h5 class='section'>Property:</h5> 050 * <ul class='spaced-list'> 051 * <li><b>ID:</b> {@link org.apache.juneau.config.store.ConfigFileStore#FILESTORE_directory FILESTORE_directory} 052 * <li><b>Name:</b> <js>"ConfigFileStore.directory.s"</js> 053 * <li><b>Data type:</b> <c>String</c> 054 * <li><b>System property:</b> <c>ConfigFileStore.directory</c> 055 * <li><b>Environment variable:</b> <c>CONFIGFILESTORE_DIRECTORY</c> 056 * <li><b>Default:</b> <js>"."</js> 057 * <li><b>Methods:</b> 058 * <ul> 059 * <li class='jm'>{@link org.apache.juneau.config.store.ConfigFileStoreBuilder#directory(String)} 060 * <li class='jm'>{@link org.apache.juneau.config.store.ConfigFileStoreBuilder#directory(File)} 061 * </ul> 062 * </ul> 063 * 064 * <h5 class='section'>Description:</h5> 065 * <p> 066 * Identifies the path of the directory containing the configuration files. 067 */ 068 public static final String FILESTORE_directory = PREFIX + ".directory.s"; 069 070 /** 071 * Configuration property: Charset. 072 * 073 * <h5 class='section'>Property:</h5> 074 * <ul class='spaced-list'> 075 * <li><b>ID:</b> {@link org.apache.juneau.config.store.ConfigFileStore#FILESTORE_charset FILESTORE_charset} 076 * <li><b>Name:</b> <js>"ConfigFileStore.charset.s"</js> 077 * <li><b>Data type:</b> {@link java.nio.charset.Charset} 078 * <li><b>System property:</b> <c>ConfigFileStore.charset</c> 079 * <li><b>Environment variable:</b> <c>CONFIGFILESTORE_CHARSET</c> 080 * <li><b>Default:</b> {@link java.nio.charset.Charset#defaultCharset()} 081 * <li><b>Methods:</b> 082 * <ul> 083 * <li class='jm'>{@link org.apache.juneau.config.store.ConfigFileStoreBuilder#charset(String)} 084 * <li class='jm'>{@link org.apache.juneau.config.store.ConfigFileStoreBuilder#charset(Charset)} 085 * </ul> 086 * </ul> 087 * 088 * <h5 class='section'>Description:</h5> 089 * <p> 090 * Identifies the charset of external files. 091 */ 092 public static final String FILESTORE_charset = PREFIX + ".charset.s"; 093 094 /** 095 * Configuration property: Use watcher. 096 * 097 * <h5 class='section'>Property:</h5> 098 * <ul class='spaced-list'> 099 * <li><b>ID:</b> {@link org.apache.juneau.config.store.ConfigFileStore#FILESTORE_useWatcher FILESTORE_useWatcher} 100 * <li><b>Name:</b> <js>"ConfigFileStore.useWatcher.b"</js> 101 * <li><b>Data type:</b> <jk>boolean</jk> 102 * <li><b>System property:</b> <c>ConfigFileStore.useWatcher</c> 103 * <li><b>Environment variable:</b> <c>CONFIGFILESTORE_USEWATCHER</c> 104 * <li><b>Default:</b> <jk>false</jk> 105 * <li><b>Methods:</b> 106 * <ul> 107 * <li class='jm'>{@link org.apache.juneau.config.store.ConfigFileStoreBuilder#useWatcher()} 108 * </ul> 109 * </ul> 110 * 111 * <h5 class='section'>Description:</h5> 112 * <p> 113 * Use a file system watcher for file system changes. 114 * 115 * <ul class='notes'> 116 * <li>Calling {@link #close()} on this object closes the watcher. 117 * </ul> 118 */ 119 public static final String FILESTORE_useWatcher = PREFIX + ".useWatcher.s"; 120 121 /** 122 * Configuration property: Watcher sensitivity. 123 * 124 * <h5 class='section'>Property:</h5> 125 * <ul class='spaced-list'> 126 * <li><b>ID:</b> {@link org.apache.juneau.config.store.ConfigFileStore#FILESTORE_watcherSensitivity FILESTORE_watcherSensitivity} 127 * <li><b>Name:</b> <js>"ConfigFileStore.watcherSensitivity.s"</js> 128 * <li><b>Data type:</b> {@link org.apache.juneau.config.store.WatcherSensitivity} 129 * <li><b>System property:</b> <c>ConfigFileStore.watcherSensitivity</c> 130 * <li><b>Environment variable:</b> <c>CONFIGFILESTORE_WATCHERSENSITIVITY</c> 131 * <li><b>Default:</b> {@link org.apache.juneau.config.store.WatcherSensitivity#MEDIUM} 132 * <li><b>Methods:</b> 133 * <ul> 134 * <li class='jm'>{@link org.apache.juneau.config.store.ConfigFileStoreBuilder#watcherSensitivity(WatcherSensitivity)} 135 * <li class='jm'>{@link org.apache.juneau.config.store.ConfigFileStoreBuilder#watcherSensitivity(String)} 136 * </ul> 137 * </ul> 138 * 139 * <h5 class='section'>Description:</h5> 140 * <p> 141 * Determines how frequently the file system is polled for updates. 142 * 143 * <ul class='notes'> 144 * <li>This relies on internal Sun packages and may not work on all JVMs. 145 * </ul> 146 */ 147 public static final String FILESTORE_watcherSensitivity = PREFIX + ".watcherSensitivity.s"; 148 149 /** 150 * Configuration property: Update-on-write. 151 * 152 * <h5 class='section'>Property:</h5> 153 * <ul class='spaced-list'> 154 * <li><b>ID:</b> {@link org.apache.juneau.config.store.ConfigFileStore#FILESTORE_updateOnWrite FILESTORE_updateOnWrite} 155 * <li><b>Name:</b> <js>"ConfigFileStore.updateOnWrite.b"</js> 156 * <li><b>Data type:</b> <jk>boolean</jk> 157 * <li><b>System property:</b> <c>ConfigFileStore.updateOnWrite</c> 158 * <li><b>Environment variable:</b> <c>CONFIGFILESTORE_UPDATEONWRITE</c> 159 * <li><b>Default:</b> <jk>false</jk> 160 * <li><b>Methods:</b> 161 * <ul> 162 * <li class='jm'>{@link org.apache.juneau.config.store.ConfigFileStoreBuilder#updateOnWrite()} 163 * </ul> 164 * </ul> 165 * 166 * <h5 class='section'>Description:</h5> 167 * <p> 168 * When enabled, the {@link #update(String, String)} method will be called immediately following 169 * calls to {@link #write(String, String, String)} when the contents are changing. 170 * <br>This allows for more immediate responses to configuration changes on file systems that use 171 * polling watchers. 172 * <br>This may cause double-triggering of {@link ConfigStoreListener ConfigStoreListeners}. 173 */ 174 public static final String FILESTORE_updateOnWrite = PREFIX + ".updateOnWrite.b"; 175 176 /** 177 * Configuration property: File extensions. 178 * 179 * <h5 class='section'>Property:</h5> 180 * <ul class='spaced-list'> 181 * <li><b>ID:</b> {@link org.apache.juneau.config.store.ConfigFileStore#FILESTORE_extensions FILESTORE_extensions} 182 * <li><b>Name:</b> <js>"ConfigFileStore.extensions.s"</js> 183 * <li><b>Data type:</b> <c>String</c> (comma-delimited) 184 * <li><b>System property:</b> <c>ConfigFileStore.extensions</c> 185 * <li><b>Environment variable:</b> <c>CONFIGFILESTORE_EXTENSIONS</c> 186 * <li><b>Default:</b> <js>"cfg"</js> 187 * <li><b>Methods:</b> 188 * <ul> 189 * <li class='jm'>{@link org.apache.juneau.config.store.ConfigFileStoreBuilder#extensions(String)} 190 * </ul> 191 * </ul> 192 * 193 * <h5 class='section'>Description:</h5> 194 * <p> 195 * Defines what file extensions to search for when the config name does not have an extension. 196 */ 197 public static final String FILESTORE_extensions = PREFIX + ".extensions.s"; 198 199 //------------------------------------------------------------------------------------------------------------------- 200 // Predefined instances 201 //------------------------------------------------------------------------------------------------------------------- 202 203 /** Default file store, all default values.*/ 204 public static final ConfigFileStore DEFAULT = ConfigFileStore.create().build(); 205 206 207 //------------------------------------------------------------------------------------------------------------------- 208 // Instance 209 //------------------------------------------------------------------------------------------------------------------- 210 211 /** 212 * Create a new builder for this object. 213 * 214 * @return A new builder for this object. 215 */ 216 public static ConfigFileStoreBuilder create() { 217 return new ConfigFileStoreBuilder(); 218 } 219 220 @Override /* Context */ 221 public ConfigFileStoreBuilder builder() { 222 return new ConfigFileStoreBuilder(getPropertyStore()); 223 } 224 225 private final File dir; 226 private final Charset charset; 227 private final WatcherThread watcher; 228 private final boolean updateOnWrite; 229 private final ConcurrentHashMap<String,String> cache = new ConcurrentHashMap<>(); 230 private final ConcurrentHashMap<String,String> nameCache = new ConcurrentHashMap<>(); 231 private final String[] extensions; 232 233 /** 234 * Constructor. 235 * 236 * @param ps The settings for this content store. 237 */ 238 protected ConfigFileStore(PropertyStore ps) { 239 super(ps); 240 try { 241 dir = new File(getStringProperty(FILESTORE_directory, ".")).getCanonicalFile(); 242 dir.mkdirs(); 243 charset = getProperty(FILESTORE_charset, Charset.class, Charset.defaultCharset()); 244 updateOnWrite = getBooleanProperty(FILESTORE_updateOnWrite, false); 245 extensions = getCdlProperty(FILESTORE_extensions, "cfg"); 246 WatcherSensitivity ws = getProperty(FILESTORE_watcherSensitivity, WatcherSensitivity.class, WatcherSensitivity.MEDIUM); 247 watcher = getBooleanProperty(FILESTORE_useWatcher, false) ? new WatcherThread(dir, ws) : null; 248 if (watcher != null) 249 watcher.start(); 250 251 } catch (Exception e) { 252 throw new RuntimeException(e); 253 } 254 } 255 256 @Override /* ConfigStore */ 257 public synchronized String read(String name) throws IOException { 258 name = resolveName(name); 259 260 Path p = resolveFile(name); 261 name = p.getFileName().toString(); 262 263 String s = cache.get(name); 264 if (s != null) 265 return s; 266 267 dir.mkdirs(); 268 269 // If file doesn't exist, don't trigger creation. 270 if (! Files.exists(p)) 271 return ""; 272 273 boolean isWritable = isWritable(p); 274 OpenOption[] oo = isWritable ? new OpenOption[]{READ,WRITE,CREATE} : new OpenOption[]{READ}; 275 276 try (FileChannel fc = FileChannel.open(p, oo)) { 277 try (FileLock lock = isWritable ? fc.lock() : null) { 278 ByteBuffer buf = ByteBuffer.allocate(1024); 279 StringBuilder sb = new StringBuilder(); 280 while (fc.read(buf) != -1) { 281 sb.append(charset.decode((ByteBuffer)(buf.flip()))); 282 buf.clear(); 283 } 284 s = sb.toString(); 285 cache.put(name, s); 286 } 287 } 288 289 return cache.get(name); 290 } 291 292 @Override /* ConfigStore */ 293 public synchronized String write(String name, String expectedContents, String newContents) throws IOException { 294 name = resolveName(name); 295 296 // This is a no-op. 297 if (isEquals(expectedContents, newContents)) 298 return null; 299 300 dir.mkdirs(); 301 302 Path p = resolveFile(name); 303 name = p.getFileName().toString(); 304 305 boolean exists = Files.exists(p); 306 307 // Don't create the file if we're not going to match. 308 if ((!exists) && isNotEmpty(expectedContents)) 309 return ""; 310 311 if (isWritable(p)) { 312 if (newContents == null) 313 Files.delete(p); 314 else { 315 try (FileChannel fc = FileChannel.open(p, READ, WRITE, CREATE)) { 316 try (FileLock lock = fc.lock()) { 317 String currentContents = ""; 318 if (exists) { 319 ByteBuffer buf = ByteBuffer.allocate(1024); 320 StringBuilder sb = new StringBuilder(); 321 while (fc.read(buf) != -1) { 322 sb.append(charset.decode((ByteBuffer)(buf.flip()))); 323 buf.clear(); 324 } 325 currentContents = sb.toString(); 326 } 327 if (expectedContents != null && ! isEquals(currentContents, expectedContents)) { 328 if (currentContents == null) 329 cache.remove(name); 330 else 331 cache.put(name, currentContents); 332 return currentContents; 333 } 334 fc.position(0); 335 fc.write(charset.encode(newContents)); 336 } 337 } 338 } 339 } 340 341 if (updateOnWrite) 342 update(name, newContents); 343 else 344 cache.remove(name); // Invalidate the cache. 345 346 return null; 347 } 348 349 @Override /* ConfigStore */ 350 public synchronized boolean exists(String name) { 351 return Files.exists(resolveFile(name)); 352 } 353 354 private Path resolveFile(String name) { 355 return dir.toPath().resolve(resolveName(name)); 356 } 357 358 @Override 359 protected String resolveName(String name) { 360 if (! nameCache.containsKey(name)) { 361 String n = null; 362 363 // Does file exist as-is? 364 if (FileUtils.exists(dir, name)) 365 n = name; 366 367 // Does name already have an extension? 368 if (n == null) { 369 for (String ext : extensions) { 370 if (FileUtils.hasExtension(name, ext)) { 371 n = name; 372 break; 373 } 374 } 375 } 376 377 // Find file with the correct extension. 378 if (n == null) { 379 for (String ext : extensions) { 380 if (FileUtils.exists(dir, name + '.' + ext)) { 381 n = name + '.' + ext; 382 break; 383 } 384 } 385 } 386 387 // If file not found, use the default which is the name with the first extension. 388 if (n == null) 389 n = extensions.length == 0 ? name : (name + "." + extensions[0]); 390 391 nameCache.put(name, n); 392 } 393 return nameCache.get(name); 394 } 395 396 private synchronized boolean isWritable(Path p) { 397 try { 398 if (! Files.exists(p)) { 399 Files.createDirectories(p.getParent()); 400 if (! Files.exists(p)) 401 p.toFile().createNewFile(); 402 } 403 } catch (IOException e) { 404 return false; 405 } 406 return Files.isWritable(p); 407 } 408 409 @Override /* ConfigStore */ 410 public synchronized ConfigFileStore update(String name, String newContents) { 411 cache.put(name, newContents); 412 super.update(name, newContents); 413 return this; 414 } 415 416 @Override /* Closeable */ 417 public synchronized void close() { 418 if (watcher != null) 419 watcher.interrupt(); 420 } 421 422 423 //--------------------------------------------------------------------------------------------- 424 // WatcherThread 425 //--------------------------------------------------------------------------------------------- 426 427 final class WatcherThread extends Thread { 428 private final WatchService watchService; 429 430 WatcherThread(File dir, WatcherSensitivity s) throws Exception { 431 watchService = FileSystems.getDefault().newWatchService(); 432 WatchEvent.Kind<?>[] kinds = new WatchEvent.Kind[]{ENTRY_CREATE, ENTRY_DELETE, ENTRY_MODIFY}; 433 WatchEvent.Modifier modifier = lookupModifier(s); 434 dir.toPath().register(watchService, kinds, modifier); 435 } 436 437 @SuppressWarnings("restriction") 438 private WatchEvent.Modifier lookupModifier(WatcherSensitivity s) { 439 try { 440 switch(s) { 441 case LOW: return com.sun.nio.file.SensitivityWatchEventModifier.LOW; 442 case MEDIUM: return com.sun.nio.file.SensitivityWatchEventModifier.MEDIUM; 443 case HIGH: return com.sun.nio.file.SensitivityWatchEventModifier.HIGH; 444 } 445 } catch (Exception e) { 446 /* Ignore */ 447 } 448 return null; 449 450 } 451 452 @SuppressWarnings("unchecked") 453 @Override /* Thread */ 454 public void run() { 455 try { 456 WatchKey key; 457 while ((key = watchService.take()) != null) { 458 for (WatchEvent<?> event : key.pollEvents()) { 459 WatchEvent.Kind<?> kind = event.kind(); 460 if (kind != OVERFLOW) 461 ConfigFileStore.this.onFileEvent(((WatchEvent<Path>)event)); 462 } 463 if (! key.reset()) 464 break; 465 } 466 } catch (Exception e) { 467 e.printStackTrace(); 468 throw new RuntimeException(e); 469 } 470 }; 471 472 @Override /* Thread */ 473 public void interrupt() { 474 try { 475 watchService.close(); 476 } catch (IOException e) { 477 throw new RuntimeException(e); 478 } finally { 479 super.interrupt(); 480 } 481 } 482 } 483 484 /** 485 * Gets called when the watcher service on this store is triggered with a file system change. 486 * 487 * @param e The file system event. 488 * @throws IOException Thrown by underlying stream. 489 */ 490 protected synchronized void onFileEvent(WatchEvent<Path> e) throws IOException { 491 String fn = e.context().getFileName().toString(); 492 493 String oldContents = cache.get(fn); 494 cache.remove(fn); 495 String newContents = read(fn); 496 if (! isEquals(oldContents, newContents)) { 497 update(fn, newContents); 498 } 499 } 500 501 //----------------------------------------------------------------------------------------------------------------- 502 // Other methods. 503 //----------------------------------------------------------------------------------------------------------------- 504 505 @Override /* Context */ 506 public OMap toMap() { 507 return super.toMap() 508 .a("ConfigFileStore", new DefaultFilteringOMap() 509 .a("charset", charset) 510 .a("extensions", extensions) 511 .a("updateOnWrite", updateOnWrite) 512 ); 513 } 514}