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