blob: 3f2d9464c409c2cb03538ffce6942f05eaa0365f [file] [log] [blame]
// ***************************************************************************************************************************
// * Licensed to the Apache Software Foundation (ASF) under one or more contributor license agreements. See the NOTICE file *
// * distributed with this work for additional information regarding copyright ownership. The ASF licenses this file *
// * to you under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance *
// * with the License. You may obtain a copy of the License at *
// * *
// * http://www.apache.org/licenses/LICENSE-2.0 *
// * *
// * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an *
// * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the *
// * specific language governing permissions and limitations under the License. *
// ***************************************************************************************************************************
package org.apache.juneau.config.store;
import static java.nio.file.StandardWatchEventKinds.*;
import static java.nio.file.StandardOpenOption.*;
import static org.apache.juneau.internal.StringUtils.*;
import java.io.*;
import java.nio.*;
import java.nio.channels.*;
import java.nio.charset.*;
import java.nio.file.*;
import java.util.concurrent.*;
import org.apache.juneau.*;
import org.apache.juneau.annotation.*;
import org.apache.juneau.internal.*;
/**
* Filesystem-based storage location for configuration files.
*
* <p>
* Points to a file system directory containing configuration files.
*/
@ConfigurableContext
public class ConfigFileStore extends ConfigStore {
//-------------------------------------------------------------------------------------------------------------------
// Configurable properties
//-------------------------------------------------------------------------------------------------------------------
static final String PREFIX = "ConfigFileStore";
/**
* Configuration property: Local file system directory.
*
* <h5 class='section'>Property:</h5>
* <ul>
* <li><b>Name:</b> <js>"ConfigFileStore.directory.s"</js>
* <li><b>Data type:</b> <c>String</c>
* <li><b>Default:</b> <js>"."</js>
* <li><b>Methods:</b>
* <ul>
* <li class='jm'>{@link ConfigFileStoreBuilder#directory(String)}
* <li class='jm'>{@link ConfigFileStoreBuilder#directory(File)}
* </ul>
* </ul>
*
* <h5 class='section'>Description:</h5>
* <p>
* Identifies the path of the directory containing the configuration files.
*/
public static final String FILESTORE_directory = PREFIX + ".directory.s";
/**
* Configuration property: Charset.
*
* <h5 class='section'>Property:</h5>
* <ul>
* <li><b>Name:</b> <js>"ConfigFileStore.charset.s"</js>
* <li><b>Data type:</b> {@link Charset}
* <li><b>Default:</b> {@link Charset#defaultCharset()}
* <li><b>Methods:</b>
* <ul>
* <li class='jm'>{@link ConfigFileStoreBuilder#charset(String)}
* <li class='jm'>{@link ConfigFileStoreBuilder#charset(Charset)}
* </ul>
* </ul>
*
* <h5 class='section'>Description:</h5>
* <p>
* Identifies the charset of external files.
*/
public static final String FILESTORE_charset = PREFIX + ".charset.s";
/**
* Configuration property: Use watcher.
*
* <h5 class='section'>Property:</h5>
* <ul>
* <li><b>Name:</b> <js>"ConfigFileStore.useWatcher.b"</js>
* <li><b>Data type:</b> <c>Boolean</c>
* <li><b>Default:</b> <jk>false</jk>
* <li><b>Methods:</b>
* <ul>
* <li class='jm'>{@link ConfigFileStoreBuilder#useWatcher()}
* </ul>
* </ul>
*
* <h5 class='section'>Description:</h5>
* <p>
* Use a file system watcher for file system changes.
*
* <ul class='notes'>
* <li>Calling {@link #close()} on this object closes the watcher.
* </ul>
*/
public static final String FILESTORE_useWatcher = PREFIX + ".useWatcher.s";
/**
* Configuration property: Watcher sensitivity.
*
* <h5 class='section'>Property:</h5>
* <ul>
* <li><b>Name:</b> <js>"ConfigFileStore.watcherSensitivity.s"</js>
* <li><b>Data type:</b> {@link WatcherSensitivity}
* <li><b>Default:</b> {@link WatcherSensitivity#MEDIUM}
* <li><b>Methods:</b>
* <ul>
* <li class='jm'>{@link ConfigFileStoreBuilder#watcherSensitivity(WatcherSensitivity)}
* <li class='jm'>{@link ConfigFileStoreBuilder#watcherSensitivity(String)}
* </ul>
* </ul>
*
* <h5 class='section'>Description:</h5>
* <p>
* Determines how frequently the file system is polled for updates.
*
* <ul class='notes'>
* <li>This relies on internal Sun packages and may not work on all JVMs.
* </ul>
*/
public static final String FILESTORE_watcherSensitivity = PREFIX + ".watcherSensitivity.s";
/**
* Configuration property: Update-on-write.
*
* <h5 class='section'>Property:</h5>
* <ul>
* <li><b>Name:</b> <js>"ConfigFileStore.updateOnWrite.b"</js>
* <li><b>Data type:</b> <c>Boolean</c>
* <li><b>Default:</b> <jk>false</jk>
* <li><b>Methods:</b>
* <ul>
* <li class='jm'>{@link ConfigFileStoreBuilder#updateOnWrite()}
* </ul>
* </ul>
*
* <h5 class='section'>Description:</h5>
* <p>
* When enabled, the {@link #update(String, String)} method will be called immediately following
* calls to {@link #write(String, String, String)} when the contents are changing.
* <br>This allows for more immediate responses to configuration changes on file systems that use
* polling watchers.
* <br>This may cause double-triggering of {@link ConfigStoreListener ConfigStoreListeners}.
*/
public static final String FILESTORE_updateOnWrite = PREFIX + ".updateOnWrite.b";
/**
* Configuration property: File extensions.
*
* <h5 class='section'>Property:</h5>
* <ul>
* <li><b>Name:</b> <js>"ConfigFileStore.extensions.s"</js>
* <li><b>Data type:</b> <c>String</c> (comma-delimited list)
* <li><b>Default:</b> <jk>"cfg"</jk>
* <li><b>Methods:</b>
* <ul>
* <li class='jm'>{@link ConfigFileStoreBuilder#extensions(String)}
* </ul>
* </ul>
*
* <h5 class='section'>Description:</h5>
* <p>
* Defines what file extensions to search for when the config name does not have an extension.
*/
public static final String FILESTORE_extensions = PREFIX + ".extensions.s";
//-------------------------------------------------------------------------------------------------------------------
// Predefined instances
//-------------------------------------------------------------------------------------------------------------------
/** Default file store, all default values.*/
public static final ConfigFileStore DEFAULT = ConfigFileStore.create().build();
//-------------------------------------------------------------------------------------------------------------------
// Instance
//-------------------------------------------------------------------------------------------------------------------
/**
* Create a new builder for this object.
*
* @return A new builder for this object.
*/
public static ConfigFileStoreBuilder create() {
return new ConfigFileStoreBuilder();
}
@Override /* Context */
public ConfigFileStoreBuilder builder() {
return new ConfigFileStoreBuilder(getPropertyStore());
}
private final File dir;
private final Charset charset;
private final WatcherThread watcher;
private final boolean updateOnWrite;
private final ConcurrentHashMap<String,String> cache = new ConcurrentHashMap<>();
private final ConcurrentHashMap<String,String> nameCache = new ConcurrentHashMap<>();
private final String[] extensions;
/**
* Constructor.
*
* @param ps The settings for this content store.
*/
protected ConfigFileStore(PropertyStore ps) {
super(ps);
try {
dir = new File(getStringProperty(FILESTORE_directory, ".")).getCanonicalFile();
dir.mkdirs();
charset = getProperty(FILESTORE_charset, Charset.class, Charset.defaultCharset());
updateOnWrite = getBooleanProperty(FILESTORE_updateOnWrite, false);
extensions = getCdlProperty(FILESTORE_extensions, "cfg");
WatcherSensitivity ws = getProperty(FILESTORE_watcherSensitivity, WatcherSensitivity.class, WatcherSensitivity.MEDIUM);
watcher = getBooleanProperty(FILESTORE_useWatcher, false) ? new WatcherThread(dir, ws) : null;
if (watcher != null)
watcher.start();
} catch (Exception e) {
throw new RuntimeException(e);
}
}
@Override /* ConfigStore */
public synchronized String read(String name) throws IOException {
name = resolveName(name);
Path p = resolveFile(name);
name = p.getFileName().toString();
String s = cache.get(name);
if (s != null)
return s;
dir.mkdirs();
// If file doesn't exist, don't trigger creation.
if (! Files.exists(p))
return "";
boolean isWritable = isWritable(p);
OpenOption[] oo = isWritable ? new OpenOption[]{READ,WRITE,CREATE} : new OpenOption[]{READ};
try (FileChannel fc = FileChannel.open(p, oo)) {
try (FileLock lock = isWritable ? fc.lock() : null) {
ByteBuffer buf = ByteBuffer.allocate(1024);
StringBuilder sb = new StringBuilder();
while (fc.read(buf) != -1) {
sb.append(charset.decode((ByteBuffer)(buf.flip())));
buf.clear();
}
s = sb.toString();
cache.put(name, s);
}
}
return cache.get(name);
}
@Override /* ConfigStore */
public synchronized String write(String name, String expectedContents, String newContents) throws IOException {
name = resolveName(name);
// This is a no-op.
if (isEquals(expectedContents, newContents))
return null;
dir.mkdirs();
Path p = resolveFile(name);
name = p.getFileName().toString();
boolean exists = Files.exists(p);
// Don't create the file if we're not going to match.
if ((!exists) && isNotEmpty(expectedContents))
return "";
if (isWritable(p)) {
if (newContents == null)
Files.delete(p);
else {
try (FileChannel fc = FileChannel.open(p, READ, WRITE, CREATE)) {
try (FileLock lock = fc.lock()) {
String currentContents = "";
if (exists) {
ByteBuffer buf = ByteBuffer.allocate(1024);
StringBuilder sb = new StringBuilder();
while (fc.read(buf) != -1) {
sb.append(charset.decode((ByteBuffer)(buf.flip())));
buf.clear();
}
currentContents = sb.toString();
}
if (expectedContents != null && ! isEquals(currentContents, expectedContents)) {
if (currentContents == null)
cache.remove(name);
else
cache.put(name, currentContents);
return currentContents;
}
fc.position(0);
fc.write(charset.encode(newContents));
}
}
}
}
if (updateOnWrite)
update(name, newContents);
else
cache.remove(name); // Invalidate the cache.
return null;
}
@Override /* ConfigStore */
public synchronized boolean exists(String name) {
return Files.exists(resolveFile(name));
}
private Path resolveFile(String name) {
return dir.toPath().resolve(resolveName(name));
}
@Override
protected String resolveName(String name) {
if (! nameCache.containsKey(name)) {
String n = null;
// Does file exist as-is?
if (FileUtils.exists(dir, name))
n = name;
// Does name already have an extension?
if (n == null) {
for (String ext : extensions) {
if (FileUtils.hasExtension(name, ext)) {
n = name;
break;
}
}
}
// Find file with the correct extension.
if (n == null) {
for (String ext : extensions) {
if (FileUtils.exists(dir, name + '.' + ext)) {
n = name + '.' + ext;
break;
}
}
}
// If file not found, use the default which is the name with the first extension.
if (n == null)
n = extensions.length == 0 ? name : (name + "." + extensions[0]);
nameCache.put(name, n);
}
return nameCache.get(name);
}
private synchronized boolean isWritable(Path p) {
try {
if (! Files.exists(p)) {
Files.createDirectories(p.getParent());
if (! Files.exists(p))
p.toFile().createNewFile();
}
} catch (IOException e) {
return false;
}
return Files.isWritable(p);
}
@Override /* ConfigStore */
public synchronized ConfigFileStore update(String name, String newContents) {
cache.put(name, newContents);
super.update(name, newContents);
return this;
}
@Override /* Closeable */
public synchronized void close() {
if (watcher != null)
watcher.interrupt();
}
//---------------------------------------------------------------------------------------------
// WatcherThread
//---------------------------------------------------------------------------------------------
final class WatcherThread extends Thread {
private final WatchService watchService;
WatcherThread(File dir, WatcherSensitivity s) throws Exception {
watchService = FileSystems.getDefault().newWatchService();
WatchEvent.Kind<?>[] kinds = new WatchEvent.Kind[]{ENTRY_CREATE, ENTRY_DELETE, ENTRY_MODIFY};
WatchEvent.Modifier modifier = lookupModifier(s);
dir.toPath().register(watchService, kinds, modifier);
}
@SuppressWarnings("restriction")
private WatchEvent.Modifier lookupModifier(WatcherSensitivity s) {
try {
switch(s) {
case LOW: return com.sun.nio.file.SensitivityWatchEventModifier.LOW;
case MEDIUM: return com.sun.nio.file.SensitivityWatchEventModifier.MEDIUM;
case HIGH: return com.sun.nio.file.SensitivityWatchEventModifier.HIGH;
}
} catch (Exception e) {
/* Ignore */
}
return null;
}
@SuppressWarnings("unchecked")
@Override /* Thread */
public void run() {
try {
WatchKey key;
while ((key = watchService.take()) != null) {
for (WatchEvent<?> event : key.pollEvents()) {
WatchEvent.Kind<?> kind = event.kind();
if (kind != OVERFLOW)
ConfigFileStore.this.onFileEvent(((WatchEvent<Path>)event));
}
if (! key.reset())
break;
}
} catch (Exception e) {
e.printStackTrace();
throw new RuntimeException(e);
}
};
@Override /* Thread */
public void interrupt() {
try {
watchService.close();
} catch (IOException e) {
throw new RuntimeException(e);
} finally {
super.interrupt();
}
}
}
/**
* Gets called when the watcher service on this store is triggered with a file system change.
*
* @param e The file system event.
* @throws IOException Thrown by underlying stream.
*/
protected synchronized void onFileEvent(WatchEvent<Path> e) throws IOException {
String fn = e.context().getFileName().toString();
String oldContents = cache.get(fn);
cache.remove(fn);
String newContents = read(fn);
if (! isEquals(oldContents, newContents)) {
update(fn, newContents);
}
}
//-----------------------------------------------------------------------------------------------------------------
// Other methods.
//-----------------------------------------------------------------------------------------------------------------
@Override /* Context */
public ObjectMap toMap() {
return super.toMap()
.append("ConfigFileStore", new DefaultFilteringObjectMap()
.append("charset", charset)
.append("extensions", extensions)
.append("updateOnWrite", updateOnWrite)
);
}
}