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.*; 027 028/** 029 * Filesystem-based storage location for configuration files. 030 * 031 * <p> 032 * Points to a file system directory containing configuration files. 033 */ 034public class ConfigFileStore extends ConfigStore { 035 036 //------------------------------------------------------------------------------------------------------------------- 037 // Configurable properties 038 //------------------------------------------------------------------------------------------------------------------- 039 040 private static final String PREFIX = "ConfigFileStore."; 041 042 /** 043 * Configuration property: Local file system directory. 044 * 045 * <h5 class='section'>Property:</h5> 046 * <ul> 047 * <li><b>Name:</b> <js>"ConfigFileStore.directory.s"</js> 048 * <li><b>Data type:</b> <code>String</code> 049 * <li><b>Default:</b> <js>"."</js> 050 * <li><b>Methods:</b> 051 * <ul> 052 * <li class='jm'>{@link ConfigFileStoreBuilder#directory(String)} 053 * <li class='jm'>{@link ConfigFileStoreBuilder#directory(File)} 054 * </ul> 055 * </ul> 056 * 057 * <h5 class='section'>Description:</h5> 058 * <p> 059 * Identifies the path of the directory containing the configuration files. 060 */ 061 public static final String FILESTORE_directory = PREFIX + "directory.s"; 062 063 /** 064 * Configuration property: Charset. 065 * 066 * <h5 class='section'>Property:</h5> 067 * <ul> 068 * <li><b>Name:</b> <js>"ConfigFileStore.charset.s"</js> 069 * <li><b>Data type:</b> {@link Charset} 070 * <li><b>Default:</b> {@link Charset#defaultCharset()} 071 * <li><b>Methods:</b> 072 * <ul> 073 * <li class='jm'>{@link ConfigFileStoreBuilder#charset(String)} 074 * <li class='jm'>{@link ConfigFileStoreBuilder#charset(Charset)} 075 * </ul> 076 * </ul> 077 * 078 * <h5 class='section'>Description:</h5> 079 * <p> 080 * Identifies the charset of external files. 081 */ 082 public static final String FILESTORE_charset = PREFIX + "charset.s"; 083 084 /** 085 * Configuration property: Use watcher. 086 * 087 * <h5 class='section'>Property:</h5> 088 * <ul> 089 * <li><b>Name:</b> <js>"ConfigFileStore.useWatcher.b"</js> 090 * <li><b>Data type:</b> <code>Boolean</code> 091 * <li><b>Default:</b> <jk>false</jk> 092 * <li><b>Methods:</b> 093 * <ul> 094 * <li class='jm'>{@link ConfigFileStoreBuilder#useWatcher()} 095 * </ul> 096 * </ul> 097 * 098 * <h5 class='section'>Description:</h5> 099 * <p> 100 * Use a file system watcher for file system changes. 101 * 102 * <h5 class='section'>Notes:</h5> 103 * <ul class='spaced-list'> 104 * <li>Calling {@link #close()} on this object closes the watcher. 105 * </ul> 106 */ 107 public static final String FILESTORE_useWatcher = PREFIX + "useWatcher.s"; 108 109 /** 110 * Configuration property: Watcher sensitivity. 111 * 112 * <h5 class='section'>Property:</h5> 113 * <ul> 114 * <li><b>Name:</b> <js>"ConfigFileStore.watcherSensitivity.s"</js> 115 * <li><b>Data type:</b> {@link WatcherSensitivity} 116 * <li><b>Default:</b> {@link WatcherSensitivity#MEDIUM} 117 * <li><b>Methods:</b> 118 * <ul> 119 * <li class='jm'>{@link ConfigFileStoreBuilder#watcherSensitivity(WatcherSensitivity)} 120 * <li class='jm'>{@link ConfigFileStoreBuilder#watcherSensitivity(String)} 121 * </ul> 122 * </ul> 123 * 124 * <h5 class='section'>Description:</h5> 125 * <p> 126 * Determines how frequently the file system is polled for updates. 127 * 128 * <h5 class='section'>Notes:</h5> 129 * <ul class='spaced-list'> 130 * <li>This relies on internal Sun packages and may not work on all JVMs. 131 * </ul> 132 */ 133 public static final String FILESTORE_watcherSensitivity = PREFIX + "watcherSensitivity.s"; 134 135 /** 136 * Configuration property: Update-on-write. 137 * 138 * <h5 class='section'>Property:</h5> 139 * <ul> 140 * <li><b>Name:</b> <js>"ConfigFileStore.updateOnWrite.b"</js> 141 * <li><b>Data type:</b> <code>Boolean</code> 142 * <li><b>Default:</b> <jk>false</jk> 143 * <li><b>Methods:</b> 144 * <ul> 145 * <li class='jm'>{@link ConfigFileStoreBuilder#updateOnWrite()} 146 * </ul> 147 * </ul> 148 * 149 * <h5 class='section'>Description:</h5> 150 * <p> 151 * When enabled, the {@link #update(String, String)} method will be called immediately following 152 * calls to {@link #write(String, String, String)} when the contents are changing. 153 * <br>This allows for more immediate responses to configuration changes on file systems that use 154 * polling watchers. 155 * <br>This may cause double-triggering of {@link ConfigStoreListener ConfigStoreListeners}. 156 */ 157 public static final String FILESTORE_updateOnWrite = PREFIX + "updateOnWrite.b"; 158 159 160 //------------------------------------------------------------------------------------------------------------------- 161 // Predefined instances 162 //------------------------------------------------------------------------------------------------------------------- 163 164 /** Default file store, all default values.*/ 165 public static final ConfigFileStore DEFAULT = ConfigFileStore.create().build(); 166 167 168 //------------------------------------------------------------------------------------------------------------------- 169 // Instance 170 //------------------------------------------------------------------------------------------------------------------- 171 172 /** 173 * Create a new builder for this object. 174 * 175 * @return A new builder for this object. 176 */ 177 public static ConfigFileStoreBuilder create() { 178 return new ConfigFileStoreBuilder(); 179 } 180 181 @Override /* Context */ 182 public ConfigFileStoreBuilder builder() { 183 return new ConfigFileStoreBuilder(getPropertyStore()); 184 } 185 186 private final File dir; 187 private final Charset charset; 188 private final WatcherThread watcher; 189 private final boolean updateOnWrite; 190 private final ConcurrentHashMap<String,String> cache = new ConcurrentHashMap<>(); 191 192 /** 193 * Constructor. 194 * 195 * @param ps The settings for this content store. 196 */ 197 protected ConfigFileStore(PropertyStore ps) { 198 super(ps); 199 try { 200 dir = new File(getStringProperty(FILESTORE_directory, ".")).getCanonicalFile(); 201 dir.mkdirs(); 202 charset = getProperty(FILESTORE_charset, Charset.class, Charset.defaultCharset()); 203 updateOnWrite = getBooleanProperty(FILESTORE_updateOnWrite, false); 204 WatcherSensitivity ws = getProperty(FILESTORE_watcherSensitivity, WatcherSensitivity.class, WatcherSensitivity.MEDIUM); 205 watcher = getBooleanProperty(FILESTORE_useWatcher, false) ? new WatcherThread(dir, ws) : null; 206 if (watcher != null) 207 watcher.start(); 208 } catch (Exception e) { 209 throw new RuntimeException(e); 210 } 211 } 212 213 @Override /* ConfigStore */ 214 public synchronized String read(String name) throws IOException { 215 String s = cache.get(name); 216 if (s != null) 217 return s; 218 219 dir.mkdirs(); 220 221 // If file doesn't exist, don't trigger creation. 222 Path p = dir.toPath().resolve(name); 223 if (! Files.exists(p)) 224 return ""; 225 226 boolean isWritable = isWritable(p); 227 OpenOption[] oo = isWritable ? new OpenOption[]{READ,WRITE,CREATE} : new OpenOption[]{READ}; 228 229 try (FileChannel fc = FileChannel.open(p, oo)) { 230 try (FileLock lock = isWritable ? fc.lock() : null) { 231 ByteBuffer buf = ByteBuffer.allocate(1024); 232 StringBuilder sb = new StringBuilder(); 233 while (fc.read(buf) != -1) { 234 sb.append(charset.decode((ByteBuffer)(buf.flip()))); 235 buf.clear(); 236 } 237 s = sb.toString(); 238 cache.put(name, s); 239 } 240 } 241 242 return cache.get(name); 243 } 244 245 @Override /* ConfigStore */ 246 public synchronized String write(String name, String expectedContents, String newContents) throws IOException { 247 248 // This is a no-op. 249 if (isEquals(expectedContents, newContents)) 250 return null; 251 252 dir.mkdirs(); 253 Path p = dir.toPath().resolve(name); 254 255 boolean exists = Files.exists(p); 256 257 // Don't create the file if we're not going to match. 258 if ((!exists) && isNotEmpty(expectedContents)) 259 return ""; 260 261 if (isWritable(p)) { 262 try (FileChannel fc = FileChannel.open(p, READ, WRITE, CREATE)) { 263 try (FileLock lock = fc.lock()) { 264 String currentContents = ""; 265 if (exists) { 266 ByteBuffer buf = ByteBuffer.allocate(1024); 267 StringBuilder sb = new StringBuilder(); 268 while (fc.read(buf) != -1) { 269 sb.append(charset.decode((ByteBuffer)(buf.flip()))); 270 buf.clear(); 271 } 272 currentContents = sb.toString(); 273 } 274 if (expectedContents != null && ! isEquals(currentContents, expectedContents)) { 275 if (currentContents == null) 276 cache.remove(name); 277 else 278 cache.put(name, currentContents); 279 return currentContents; 280 } 281 fc.position(0); 282 fc.write(charset.encode(newContents)); 283 } 284 } 285 } 286 287 if (updateOnWrite) 288 update(name, newContents); 289 else 290 cache.remove(name); // Invalidate the cache. 291 292 return null; 293 } 294 295 private synchronized boolean isWritable(Path p) { 296 try { 297 if (! Files.exists(p)) { 298 Files.createDirectories(p.getParent()); 299 if (! Files.exists(p)) 300 p.toFile().createNewFile(); 301 } 302 } catch (IOException e) { 303 return false; 304 } 305 return Files.isWritable(p); 306 } 307 308 @Override /* ConfigStore */ 309 public synchronized ConfigFileStore update(String name, String newContents) { 310 cache.put(name, newContents); 311 super.update(name, newContents); 312 return this; 313 } 314 315 @Override /* Closeable */ 316 public synchronized void close() { 317 if (watcher != null) 318 watcher.interrupt(); 319 } 320 321 322 //--------------------------------------------------------------------------------------------- 323 // WatcherThread 324 //--------------------------------------------------------------------------------------------- 325 326 final class WatcherThread extends Thread { 327 private final WatchService watchService; 328 329 WatcherThread(File dir, WatcherSensitivity s) throws Exception { 330 watchService = FileSystems.getDefault().newWatchService(); 331 WatchEvent.Kind<?>[] kinds = new WatchEvent.Kind[]{ENTRY_CREATE, ENTRY_DELETE, ENTRY_MODIFY}; 332 WatchEvent.Modifier modifier = lookupModifier(s); 333 dir.toPath().register(watchService, kinds, modifier); 334 } 335 336 @SuppressWarnings("restriction") 337 private WatchEvent.Modifier lookupModifier(WatcherSensitivity s) { 338 try { 339 switch(s) { 340 case LOW: return com.sun.nio.file.SensitivityWatchEventModifier.LOW; 341 case MEDIUM: return com.sun.nio.file.SensitivityWatchEventModifier.MEDIUM; 342 case HIGH: return com.sun.nio.file.SensitivityWatchEventModifier.HIGH; 343 } 344 } catch (Exception e) { 345 /* Ignore */ 346 } 347 return null; 348 349 } 350 351 @SuppressWarnings("unchecked") 352 @Override /* Thread */ 353 public void run() { 354 try { 355 WatchKey key; 356 while ((key = watchService.take()) != null) { 357 for (WatchEvent<?> event : key.pollEvents()) { 358 WatchEvent.Kind<?> kind = event.kind(); 359 if (kind != OVERFLOW) 360 ConfigFileStore.this.onFileEvent(((WatchEvent<Path>)event)); 361 } 362 if (! key.reset()) 363 break; 364 } 365 } catch (Exception e) { 366 e.printStackTrace(); 367 throw new RuntimeException(e); 368 } 369 }; 370 371 @Override /* Thread */ 372 public void interrupt() { 373 try { 374 watchService.close(); 375 } catch (IOException e) { 376 throw new RuntimeException(e); 377 } finally { 378 super.interrupt(); 379 } 380 } 381 } 382 383 /** 384 * Gets called when the watcher service on this store is triggered with a file system change. 385 * 386 * @param e The file system event. 387 * @throws IOException 388 */ 389 protected synchronized void onFileEvent(WatchEvent<Path> e) throws IOException { 390 String fn = e.context().getFileName().toString(); 391 392 String oldContents = cache.get(fn); 393 cache.remove(fn); 394 String newContents = read(fn); 395 if (! isEquals(oldContents, newContents)) { 396 update(fn, newContents); 397 } 398 } 399}