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