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