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