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