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.collections.JsonMap.*; 018import static org.apache.juneau.common.internal.StringUtils.*; 019import static org.apache.juneau.common.internal.ThrowableUtils.*; 020 021import java.io.*; 022import java.lang.annotation.*; 023import java.lang.reflect.*; 024import java.nio.*; 025import java.nio.channels.*; 026import java.nio.charset.*; 027import java.nio.file.*; 028import java.util.concurrent.*; 029 030import org.apache.juneau.*; 031import org.apache.juneau.collections.*; 032import org.apache.juneau.internal.*; 033import org.apache.juneau.utils.*; 034 035/** 036 * Filesystem-based storage location for configuration files. 037 * 038 * <p> 039 * Points to a file system directory containing configuration files. 040 * 041 * <h5 class='section'>Notes:</h5><ul> 042 * <li class='note'>This class is thread safe and reusable. 043 * </ul> 044 */ 045public class FileStore extends ConfigStore { 046 047 //------------------------------------------------------------------------------------------------------------------- 048 // Static 049 //------------------------------------------------------------------------------------------------------------------- 050 051 /** Default file store, all default values.*/ 052 public static final FileStore DEFAULT = FileStore.create().build(); 053 054 /** 055 * Creates a new builder for this object. 056 * 057 * @return A new builder. 058 */ 059 public static Builder create() { 060 return new Builder(); 061 } 062 063 //------------------------------------------------------------------------------------------------------------------- 064 // Builder 065 //------------------------------------------------------------------------------------------------------------------- 066 067 /** 068 * Builder class. 069 */ 070 @FluentSetters 071 public static class Builder extends ConfigStore.Builder { 072 073 String directory, extensions; 074 Charset charset; 075 boolean enableWatcher, updateOnWrite; 076 WatcherSensitivity watcherSensitivity; 077 078 /** 079 * Constructor, default settings. 080 */ 081 protected Builder() { 082 super(); 083 directory = env("ConfigFileStore.directory", "."); 084 charset = env("ConfigFileStore.charset", Charset.defaultCharset()); 085 enableWatcher = env("ConfigFileStore.enableWatcher", false); 086 watcherSensitivity = env("ConfigFileStore.watcherSensitivity", WatcherSensitivity.MEDIUM); 087 updateOnWrite = env("ConfigFileStore.updateOnWrite", false); 088 extensions = env("ConfigFileStore.extensions", "cfg"); 089 } 090 091 /** 092 * Copy constructor. 093 * 094 * @param copyFrom The bean to copy from. 095 */ 096 protected Builder(FileStore copyFrom) { 097 super(copyFrom); 098 type(copyFrom.getClass()); 099 directory = copyFrom.directory; 100 charset = copyFrom.charset; 101 enableWatcher = copyFrom.enableWatcher; 102 watcherSensitivity = copyFrom.watcherSensitivity; 103 updateOnWrite = copyFrom.updateOnWrite; 104 extensions = copyFrom.extensions; 105 } 106 107 /** 108 * Copy constructor. 109 * 110 * @param copyFrom The builder to copy from. 111 */ 112 protected Builder(Builder copyFrom) { 113 super(copyFrom); 114 directory = copyFrom.directory; 115 charset = copyFrom.charset; 116 enableWatcher = copyFrom.enableWatcher; 117 watcherSensitivity = copyFrom.watcherSensitivity; 118 updateOnWrite = copyFrom.updateOnWrite; 119 extensions = copyFrom.extensions; 120 } 121 122 @Override /* Context.Builder */ 123 public Builder copy() { 124 return new Builder(this); 125 } 126 127 @Override /* Context.Builder */ 128 public FileStore build() { 129 return build(FileStore.class); 130 } 131 132 //----------------------------------------------------------------------------------------------------------------- 133 // Properties 134 //----------------------------------------------------------------------------------------------------------------- 135 136 /** 137 * Local file system directory. 138 * 139 * <p> 140 * Identifies the path of the directory containing the configuration files. 141 * 142 * @param value 143 * The new value for this property. 144 * <br>The default is the first value found: 145 * <ul> 146 * <li>System property <js>"ConfigFileStore.directory" 147 * <li>Environment variable <js>"CONFIGFILESTORE_DIRECTORY" 148 * <li><js>"."</js> 149 * </ul> 150 * @return This object. 151 */ 152 public Builder directory(String value) { 153 directory = value; 154 return this; 155 } 156 157 /** 158 * Local file system directory. 159 * 160 * <p> 161 * Identifies the path of the directory containing the configuration files. 162 * 163 * @param value 164 * The new value for this property. 165 * <br>The default is the first value found: 166 * <ul> 167 * <li>System property <js>"ConfigFileStore.directory" 168 * <li>Environment variable <js>"CONFIGFILESTORE_DIRECTORY" 169 * <li><js>"."</js>. 170 * </ul> 171 * @return This object. 172 */ 173 public Builder directory(File value) { 174 directory = value.getAbsolutePath(); 175 return this; 176 } 177 178 /** 179 * Charset for external files. 180 * 181 * <p> 182 * Identifies the charset of external files. 183 * 184 * @param value 185 * The new value for this property. 186 * <br>The default is the first value found: 187 * <ul> 188 * <li>System property <js>"ConfigFileStore.charset" 189 * <li>Environment variable <js>"CONFIGFILESTORE_CHARSET" 190 * <li>{@link Charset#defaultCharset()} 191 * </ul> 192 * @return This object. 193 */ 194 public Builder charset(Charset value) { 195 charset = value; 196 return this; 197 } 198 199 /** 200 * Use watcher. 201 * 202 * <p> 203 * Use a file system watcher for file system changes. 204 * 205 * <h5 class='section'>Notes:</h5><ul> 206 * <li class='note'>Calling {@link FileStore#close()} closes the watcher. 207 * </ul> 208 * 209 * <p> 210 * The default is the first value found: 211 * <ul> 212 * <li>System property <js>"ConfigFileStore.enableWatcher" 213 * <li>Environment variable <js>"CONFIGFILESTORE_ENABLEWATCHER" 214 * <li><jk>false</jk>. 215 * </ul> 216 * 217 * @return This object. 218 */ 219 public Builder enableWatcher() { 220 enableWatcher = true; 221 return this; 222 } 223 224 /** 225 * Watcher sensitivity. 226 * 227 * <p> 228 * Determines how frequently the file system is polled for updates. 229 * 230 * <h5 class='section'>Notes:</h5><ul> 231 * <li class='note'>This relies on internal Sun packages and may not work on all JVMs. 232 * </ul> 233 * 234 * @param value 235 * The new value for this property. 236 * <br>The default is the first value found: 237 * <ul> 238 * <li>System property <js>"ConfigFileStore.watcherSensitivity" 239 * <li>Environment variable <js>"CONFIGFILESTORE_WATCHERSENSITIVITY" 240 * <li>{@link WatcherSensitivity#MEDIUM} 241 * </ul> 242 * @return This object. 243 */ 244 public Builder watcherSensitivity(WatcherSensitivity value) { 245 watcherSensitivity = value; 246 return this; 247 } 248 249 /** 250 * Update-on-write. 251 * 252 * <p> 253 * When enabled, the {@link FileStore#update(String, String)} method will be called immediately following 254 * calls to {@link FileStore#write(String, String, String)} when the contents are changing. 255 * <br>This allows for more immediate responses to configuration changes on file systems that use 256 * polling watchers. 257 * <br>This may cause double-triggering of {@link ConfigStoreListener ConfigStoreListeners}. 258 * 259 * <p> 260 * The default is the first value found: 261 * <ul> 262 * <li>System property <js>"ConfigFileStore.updateOnWrite" 263 * <li>Environment variable <js>"CONFIGFILESTORE_UPDATEONWRITE" 264 * <li><jk>false</jk>. 265 * </ul> 266 * 267 * @return This object. 268 */ 269 public Builder updateOnWrite() { 270 updateOnWrite = true; 271 return this; 272 } 273 274 /** 275 * File extensions. 276 * 277 * <p> 278 * Defines what file extensions to search for when the config name does not have an extension. 279 * 280 * @param value 281 * The new value for this property. 282 * The default is the first value found: 283 * <ul> 284 * <li>System property <js>"ConfigFileStore.extensions" 285 * <li>Environment variable <js>"CONFIGFILESTORE_EXTENSIONS" 286 * <li><js>"cfg"</js> 287 * </ul> 288 * @return This object. 289 */ 290 public Builder extensions(String value) { 291 extensions = value; 292 return this; 293 } 294 295 // <FluentSetters> 296 297 @Override /* GENERATED - org.apache.juneau.Context.Builder */ 298 public Builder annotations(Annotation...values) { 299 super.annotations(values); 300 return this; 301 } 302 303 @Override /* GENERATED - org.apache.juneau.Context.Builder */ 304 public Builder apply(AnnotationWorkList work) { 305 super.apply(work); 306 return this; 307 } 308 309 @Override /* GENERATED - org.apache.juneau.Context.Builder */ 310 public Builder applyAnnotations(java.lang.Class<?>...fromClasses) { 311 super.applyAnnotations(fromClasses); 312 return this; 313 } 314 315 @Override /* GENERATED - org.apache.juneau.Context.Builder */ 316 public Builder applyAnnotations(Method...fromMethods) { 317 super.applyAnnotations(fromMethods); 318 return this; 319 } 320 321 @Override /* GENERATED - org.apache.juneau.Context.Builder */ 322 public Builder cache(Cache<HashKey,? extends org.apache.juneau.Context> value) { 323 super.cache(value); 324 return this; 325 } 326 327 @Override /* GENERATED - org.apache.juneau.Context.Builder */ 328 public Builder debug() { 329 super.debug(); 330 return this; 331 } 332 333 @Override /* GENERATED - org.apache.juneau.Context.Builder */ 334 public Builder debug(boolean value) { 335 super.debug(value); 336 return this; 337 } 338 339 @Override /* GENERATED - org.apache.juneau.Context.Builder */ 340 public Builder impl(Context value) { 341 super.impl(value); 342 return this; 343 } 344 345 @Override /* GENERATED - org.apache.juneau.Context.Builder */ 346 public Builder type(Class<? extends org.apache.juneau.Context> value) { 347 super.type(value); 348 return this; 349 } 350 351 // </FluentSetters> 352 } 353 354 //------------------------------------------------------------------------------------------------------------------- 355 // Instance 356 //------------------------------------------------------------------------------------------------------------------- 357 358 @Override /* Context */ 359 public Builder copy() { 360 return new Builder(this); 361 } 362 363 final String directory, extensions; 364 final Charset charset; 365 final boolean enableWatcher, updateOnWrite; 366 final WatcherSensitivity watcherSensitivity; 367 368 private final File dir; 369 private final WatcherThread watcher; 370 private final ConcurrentHashMap<String,String> cache = new ConcurrentHashMap<>(); 371 private final ConcurrentHashMap<String,String> nameCache = new ConcurrentHashMap<>(); 372 private final String[] exts; 373 374 /** 375 * Constructor. 376 * 377 * @param builder The builder for this object. 378 */ 379 public FileStore(Builder builder) { 380 super(builder); 381 directory = builder.directory; 382 extensions = builder.extensions; 383 charset = builder.charset; 384 enableWatcher = builder.enableWatcher; 385 updateOnWrite = builder.updateOnWrite; 386 watcherSensitivity = builder.watcherSensitivity; 387 try { 388 dir = new File(directory).getCanonicalFile(); 389 dir.mkdirs(); 390 exts = split(extensions); 391 watcher = enableWatcher ? new WatcherThread(dir, watcherSensitivity) : null; 392 if (watcher != null) 393 watcher.start(); 394 } catch (Exception e) { 395 throw asRuntimeException(e); 396 } 397 } 398 399 @Override /* ConfigStore */ 400 public synchronized String read(String name) throws IOException { 401 name = resolveName(name); 402 403 Path p = resolveFile(name); 404 name = p.getFileName().toString(); 405 406 String s = cache.get(name); 407 if (s != null) 408 return s; 409 410 dir.mkdirs(); 411 412 // If file doesn't exist, don't trigger creation. 413 if (! Files.exists(p)) 414 return ""; 415 416 boolean isWritable = isWritable(p); 417 OpenOption[] oo = isWritable ? new OpenOption[]{READ,WRITE,CREATE} : new OpenOption[]{READ}; 418 419 try (FileChannel fc = FileChannel.open(p, oo)) { 420 try (FileLock lock = isWritable ? fc.lock() : null) { 421 ByteBuffer buf = ByteBuffer.allocate(1024); 422 StringBuilder sb = new StringBuilder(); 423 while (fc.read(buf) != -1) { 424 sb.append(charset.decode((ByteBuffer)(((Buffer)buf).flip()))); // Fixes Java 11 issue involving overridden flip method. 425 ((Buffer)buf).clear(); 426 } 427 s = sb.toString(); 428 cache.put(name, s); 429 } 430 } 431 432 return cache.get(name); 433 } 434 435 @Override /* ConfigStore */ 436 public synchronized String write(String name, String expectedContents, String newContents) throws IOException { 437 name = resolveName(name); 438 439 // This is a no-op. 440 if (eq(expectedContents, newContents)) 441 return null; 442 443 dir.mkdirs(); 444 445 Path p = resolveFile(name); 446 name = p.getFileName().toString(); 447 448 boolean exists = Files.exists(p); 449 450 // Don't create the file if we're not going to match. 451 if ((!exists) && isNotEmpty(expectedContents)) 452 return ""; 453 454 if (isWritable(p)) { 455 if (newContents == null) 456 Files.delete(p); 457 else { 458 try (FileChannel fc = FileChannel.open(p, READ, WRITE, CREATE)) { 459 try (FileLock lock = fc.lock()) { 460 String currentContents = ""; 461 if (exists) { 462 ByteBuffer buf = ByteBuffer.allocate(1024); 463 StringBuilder sb = new StringBuilder(); 464 while (fc.read(buf) != -1) { 465 sb.append(charset.decode((ByteBuffer)((Buffer)buf).flip())); 466 ((Buffer)buf).clear(); 467 } 468 currentContents = sb.toString(); 469 } 470 if (expectedContents != null && ! eq(currentContents, expectedContents)) { 471 if (currentContents == null) 472 cache.remove(name); 473 else 474 cache.put(name, currentContents); 475 return currentContents; 476 } 477 fc.position(0); 478 fc.write(charset.encode(newContents)); 479 } 480 } 481 } 482 } 483 484 if (updateOnWrite) 485 update(name, newContents); 486 else 487 cache.remove(name); // Invalidate the cache. 488 489 return null; 490 } 491 492 @Override /* ConfigStore */ 493 public synchronized boolean exists(String name) { 494 return Files.exists(resolveFile(name)); 495 } 496 497 private Path resolveFile(String name) { 498 return dir.toPath().resolve(resolveName(name)); 499 } 500 501 @Override 502 protected String resolveName(String name) { 503 if (! nameCache.containsKey(name)) { 504 String n = null; 505 506 // Does file exist as-is? 507 if (FileUtils.exists(dir, name)) 508 n = name; 509 510 // Does name already have an extension? 511 if (n == null) { 512 for (String ext : exts) { 513 if (FileUtils.hasExtension(name, ext)) { 514 n = name; 515 break; 516 } 517 } 518 } 519 520 // Find file with the correct extension. 521 if (n == null) { 522 for (String ext : exts) { 523 if (FileUtils.exists(dir, name + '.' + ext)) { 524 n = name + '.' + ext; 525 break; 526 } 527 } 528 } 529 530 // If file not found, use the default which is the name with the first extension. 531 if (n == null) 532 n = exts.length == 0 ? name : (name + "." + exts[0]); 533 534 nameCache.put(name, n); 535 } 536 return nameCache.get(name); 537 } 538 539 private synchronized boolean isWritable(Path p) { 540 try { 541 if (! Files.exists(p)) { 542 Files.createDirectories(p.getParent()); 543 if (! Files.exists(p)) 544 p.toFile().createNewFile(); 545 } 546 } catch (IOException e) { 547 return false; 548 } 549 return Files.isWritable(p); 550 } 551 552 @Override /* ConfigStore */ 553 public synchronized FileStore update(String name, String newContents) { 554 cache.put(name, newContents); 555 super.update(name, newContents); 556 return this; 557 } 558 559 @Override /* Closeable */ 560 public synchronized void close() { 561 if (watcher != null) 562 watcher.interrupt(); 563 } 564 565 566 //--------------------------------------------------------------------------------------------- 567 // WatcherThread 568 //--------------------------------------------------------------------------------------------- 569 570 final class WatcherThread extends Thread { 571 private final WatchService watchService; 572 573 WatcherThread(File dir, WatcherSensitivity s) throws Exception { 574 watchService = FileSystems.getDefault().newWatchService(); 575 WatchEvent.Kind<?>[] kinds = new WatchEvent.Kind[]{ENTRY_CREATE, ENTRY_DELETE, ENTRY_MODIFY}; 576 WatchEvent.Modifier modifier = lookupModifier(s); 577 dir.toPath().register(watchService, kinds, modifier); 578 } 579 580 private WatchEvent.Modifier lookupModifier(WatcherSensitivity s) { 581 try { 582 switch(s) { 583 case LOW: return com.sun.nio.file.SensitivityWatchEventModifier.LOW; 584 case MEDIUM: return com.sun.nio.file.SensitivityWatchEventModifier.MEDIUM; 585 case HIGH: return com.sun.nio.file.SensitivityWatchEventModifier.HIGH; 586 } 587 } catch (Exception e) { 588 /* Ignore */ 589 } 590 return null; 591 592 } 593 594 @SuppressWarnings("unchecked") 595 @Override /* Thread */ 596 public void run() { 597 try { 598 WatchKey key; 599 while ((key = watchService.take()) != null) { 600 for (WatchEvent<?> event : key.pollEvents()) { 601 WatchEvent.Kind<?> kind = event.kind(); 602 if (kind != OVERFLOW) 603 FileStore.this.onFileEvent(((WatchEvent<Path>)event)); 604 } 605 if (! key.reset()) 606 break; 607 } 608 } catch (Exception e) { 609 throw asRuntimeException(e); 610 } 611 }; 612 613 @Override /* Thread */ 614 public void interrupt() { 615 try { 616 watchService.close(); 617 } catch (IOException e) { 618 throw asRuntimeException(e); 619 } finally { 620 super.interrupt(); 621 } 622 } 623 } 624 625 /** 626 * Gets called when the watcher service on this store is triggered with a file system change. 627 * 628 * @param e The file system event. 629 * @throws IOException Thrown by underlying stream. 630 */ 631 protected synchronized void onFileEvent(WatchEvent<Path> e) throws IOException { 632 String fn = e.context().getFileName().toString(); 633 634 String oldContents = cache.get(fn); 635 cache.remove(fn); 636 String newContents = read(fn); 637 if (! eq(oldContents, newContents)) { 638 update(fn, newContents); 639 } 640 } 641 642 //----------------------------------------------------------------------------------------------------------------- 643 // Other methods 644 //----------------------------------------------------------------------------------------------------------------- 645 646 @Override /* Context */ 647 protected JsonMap properties() { 648 return filteredMap("charset", charset, "extensions", extensions, "updateOnWrite", updateOnWrite); 649 } 650}