diff --git a/nucleus/payara-modules/nucleus-microprofile/config-service/src/main/java/fish/payara/nucleus/microprofile/config/source/DirConfigSource.java b/nucleus/payara-modules/nucleus-microprofile/config-service/src/main/java/fish/payara/nucleus/microprofile/config/source/DirConfigSource.java new file mode 100644 index 00000000000..e8bdc93f99e --- /dev/null +++ b/nucleus/payara-modules/nucleus-microprofile/config-service/src/main/java/fish/payara/nucleus/microprofile/config/source/DirConfigSource.java @@ -0,0 +1,446 @@ +/* + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER. + * + * Copyright (c) [2017-2020] Payara Foundation and/or its affiliates. All rights reserved. + * + * The contents of this file are subject to the terms of either the GNU + * General Public License Version 2 only ("GPL") or the Common Development + * and Distribution License("CDDL") (collectively, the "License"). You + * may not use this file except in compliance with the License. You can + * obtain a copy of the License at + * https://github.com/payara/Payara/blob/master/LICENSE.txt + * See the License for the specific + * language governing permissions and limitations under the License. + * + * When distributing the software, include this License Header Notice in each + * file and include the License file at glassfish/legal/LICENSE.txt. + * + * GPL Classpath Exception: + * The Payara Foundation designates this particular file as subject to the "Classpath" + * exception as provided by the Payara Foundation in the GPL Version 2 section of the License + * file that accompanied this code. + * + * Modifications: + * If applicable, add the following below the License Header, with the fields + * enclosed by brackets [] replaced by your own identifying information: + * "Portions Copyright [year] [name of copyright owner]" + * + * Contributor(s): + * If you wish your version of this file to be governed by only the CDDL or + * only the GPL Version 2, indicate your decision by adding "[Contributor] + * elects to include this software in this distribution under the [CDDL or GPL + * Version 2] license." If you don't indicate a single choice of license, a + * recipient has the option to distribute your version of this file under + * either the CDDL, the GPL Version 2 or to extend the choice of license to + * its licensees as provided above. However, if you add GPL Version 2 code + * and therefore, elected the GPL Version 2 license, then the option applies + * only if the new code is made subject to such option by the copyright + * holder. + */ +package fish.payara.nucleus.microprofile.config.source; + +import fish.payara.nucleus.microprofile.config.spi.ConfigProviderResolverImpl; +import org.eclipse.microprofile.config.spi.ConfigSource; + +import java.io.File; +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.nio.file.FileSystems; +import java.nio.file.FileVisitResult; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.nio.file.SimpleFileVisitor; +import java.nio.file.WatchEvent; +import java.nio.file.WatchKey; +import java.nio.file.WatchService; +import java.nio.file.attribute.BasicFileAttributes; +import java.nio.file.attribute.FileTime; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; +import java.util.logging.Logger; + +import static java.util.Collections.unmodifiableMap; +import static java.util.concurrent.TimeUnit.SECONDS; +import static java.util.logging.Level.SEVERE; +import static java.util.logging.Level.WARNING; +import static java.util.stream.Collectors.toMap; +import static java.nio.file.StandardWatchEventKinds.ENTRY_CREATE; +import static java.nio.file.StandardWatchEventKinds.ENTRY_DELETE; +import static java.nio.file.StandardWatchEventKinds.ENTRY_MODIFY; + +/** + * Config Source that reads properties from files from a directory + * where filename is the property name and file contents is the property value. + * @since 5.2020.7 + */ +public class DirConfigSource extends PayaraConfigSource implements ConfigSource { + + static final class DirProperty { + final String propertyValue; + final FileTime lastModifiedTime; + final Path path; + + DirProperty(String propertyValue, FileTime lastModifiedTime, Path path) { + this.propertyValue = propertyValue; + this.lastModifiedTime = lastModifiedTime; + this.path = path; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + DirProperty that = (DirProperty) o; + return propertyValue.equals(that.propertyValue) && + lastModifiedTime.equals(that.lastModifiedTime) && + path.equals(that.path); + } + + @Override + public int hashCode() { + return Objects.hash(propertyValue, lastModifiedTime, path); + } + } + + final class DirPropertyWatcher implements Runnable { + + private final Logger logger = Logger.getLogger(DirConfigSource.class.getName()); + private final WatchService watcher = FileSystems.getDefault().newWatchService(); + private final ConcurrentHashMap watchedFileKeys = new ConcurrentHashMap<>(); + + DirPropertyWatcher(Path topmostDir) throws IOException { + if (Files.exists(topmostDir) && Files.isDirectory(topmostDir) && Files.isReadable(topmostDir)) { + registerAll(topmostDir); + } else { + throw new IOException("Given directory '"+topmostDir+"' is no directory or cannot be read."); + } + } + + /** + * Register file watchers recursively (as they don't attach themselfs to sub directories...) + * and initialize values from files present and suitable. + * @param dir Topmost directory to start recursive traversal from + * @throws IOException + */ + final void registerAll(Path dir) throws IOException { + Files.walkFileTree(dir, new SimpleFileVisitor() { + @Override + public FileVisitResult preVisitDirectory(Path dir, BasicFileAttributes attrs) throws IOException + { + // only register subdirectories if the directory itself is suitable. + if ( isAptDir(dir) ) { + register(dir); + return FileVisitResult.CONTINUE; + } + return FileVisitResult.SKIP_SUBTREE; + } + + // Read and ingest all files and dirs present + @Override + public FileVisitResult visitFile(Path path, BasicFileAttributes mainAtts) throws IOException { + // file will be checked before upserting. + upsertPropertyFromPath(path, mainAtts); + return FileVisitResult.CONTINUE; + } + }); + } + + final void register(Path dir) throws IOException { + WatchKey key = dir.register(watcher, ENTRY_CREATE, ENTRY_DELETE, ENTRY_MODIFY); + watchedFileKeys.putIfAbsent(key, dir); + logger.finer("MPCONFIG DirConfigSource: registered \""+dir+"\" as key \""+key+"\"."); + } + + @Override + public final void run() { + // wait infinitely until we receive an event (or the executor is shutting down) + WatchKey key; + try { + key = watcher.take(); + } catch (InterruptedException ex) { + logger.info("MPCONFIG DirConfigSource: shutting down watcher thread."); + return; + } + + Path workDir = watchedFileKeys.get(key); + for (WatchEvent event : key.pollEvents()) { + WatchEvent.Kind kind = event.kind(); + + @SuppressWarnings("unchecked") + WatchEvent ev = (WatchEvent) event; + Path fileName = ev.context(); + Path path = workDir.resolve(fileName); + + logger.finer("MPCONFIG DirConfigSource: detected change: "+fileName.toString()+" : "+kind.toString()); + + try { + // new directory to be watched and traversed + if (kind == ENTRY_CREATE && isAptDir(path)) { + logger.finer("MPCONFIG DirConfigSource: registering new paths."); + registerAll(path); + } + // new or updated file found (new = create + modify on content save) + // or new symlink found (symlinks are create only!) (also, aptness of file is checked inside update routine) + if ( kind == ENTRY_MODIFY || (kind == ENTRY_CREATE && Files.isSymbolicLink(path)) ) { + logger.finer("MPCONFIG DirConfigSource: processing new or updated file \""+path.toString()+"\"."); + BasicFileAttributes atts = Files.readAttributes(path, BasicFileAttributes.class); + upsertPropertyFromPath(path, atts); + } + if (Files.notExists(path) && ! watchedFileKeys.containsValue(path) && kind == ENTRY_DELETE) { + logger.finer("MPCONFIG DirConfigSource: removing deleted file \""+path.toString()+"\"."); + removePropertyFromPath(path); + } + } catch (IOException e) { + logger.log(WARNING, "MPCONFIG DirConfigSource: could not process event '"+kind+"' on '"+path+"'", e); + } + } + + // Reset key (obligatory) and remove from set if directory no longer accessible + boolean valid = key.reset(); + if (!valid) { + logger.finer("MPCONFIG DirConfigSource: removing watcher for key \""+key+"\"."); + watchedFileKeys.remove(key); + } + } + } + + private static final Logger logger = Logger.getLogger(DirConfigSource.class.getName()); + private Path directory; + private final ConcurrentHashMap properties = new ConcurrentHashMap<>(); + + public DirConfigSource() { + try { + // get the directory from the app server config + this.directory = findDir(); + // create the watcher for the directory + configService.getExecutor().scheduleWithFixedDelay(createWatcher(this.directory), 0, 1, SECONDS); + } catch (IOException e) { + logger.log(SEVERE, "MPCONFIG DirConfigSource: error during setup.", e); + } + } + + // Used for testing only with explicit dependency injection + // Used for testing only with explicit dependency injection + DirConfigSource(Path directory, ConfigProviderResolverImpl configService) { + super(configService); + this.directory = directory; + } + + // Used for testing only + DirPropertyWatcher createWatcher(Path topmostDirectory) throws IOException { + return new DirPropertyWatcher(topmostDirectory); + } + + @Override + public Map getProperties() { + return unmodifiableMap(properties.entrySet().stream() + .collect(toMap(Map.Entry::getKey, e -> e.getValue().propertyValue))); + } + + // Used for testing only + void setProperties(Map properties) { + this.properties.clear(); + this.properties.putAll(properties); + } + + @Override + public Set getPropertyNames() { + return properties.keySet(); + } + + @Override + public int getOrdinal() { + return Integer.parseInt(configService.getMPConfig().getSecretDirOrdinality()); + } + + @Override + public String getValue(String property) { + DirProperty result = properties.get(property); + return result == null ? null : result.propertyValue; + } + + @Override + public String getName() { + return "Directory"; + } + + Path findDir() throws IOException { + String path = configService.getMPConfig().getSecretDir(); + List candidates = new ArrayList<>(); + + // adding all pathes where to look for the directory... + candidates.add(Paths.get(path)); + // let's try it relative to server environment root + if ( ! Paths.get(path).isAbsolute()) + candidates.add(Paths.get(System.getProperty("com.sun.aas.instanceRoot"), path).normalize()); + + for (Path candidate : candidates) { + if (isAptDir(candidate)) { + return candidate; + } + } + throw new IOException("Given MPCONFIG directory '"+path+"' is no directory, cannot be read or has a leading dot."); + } + + /** + * Upserting a property from a file. Checking for suitability of the file, too. + * @param path Path to the file + * @param mainAtts The attributes of the file (like mod time etc) + * @return true if file was suitable, false if not. + * @throws IOException In case anything goes bonkers. + */ + final boolean upsertPropertyFromPath(Path path, BasicFileAttributes mainAtts) throws IOException { + // check for a suitable file first, return aptness. + if ( isAptFile(path, mainAtts) ) { + // retrieve the property name from the file path + String property = parsePropertyNameFromPath(path, this.directory); + + // Conflict handling: + // When this property is not already present, check how to solve the conflict. + // This property file will be skipped if the file we already have is deeper in the file tree... + if (isLongestMatchForPath(property, path)) { + properties.put(property, readPropertyFromPath(path, mainAtts, this.directory)); + return true; + } + } + return false; + } + + final void removePropertyFromPath(Path path) { + String property = parsePropertyNameFromPath(path, this.directory); + // not present? go away silently. + if (! properties.containsKey(property)) return; + + // only delete from the map if the file that has been deleted is the same as the one stored in the map + // -> deleting a file less specific but matching a property should not remove from the map + // -> deleting a file more specific than in map shouldn't occur (it had to slip through longest match check then). + if (path.equals(properties.get(property).path)) { + properties.remove(property); + } + } + + /** + * Check if a new path to a given property is a more specific match for it. + * If both old and new paths are the same, it's still treated as more specific, allowing for updates. + * @param property + * @param newPath + * @return true if new path more specific or equal to old path of property, false if not. + */ + final boolean isLongestMatchForPath(String property, Path newPath) { + if (newPath == null || property == null || property.isEmpty()) + return false; + // No property -> path is new and more specific + if (! properties.containsKey(property)) + return true; + DirProperty old = properties.get(property); + return isLongestMatchForPath(this.directory, old.path, newPath); + } + + /** + * Check if the new path given is a more specific path to a property file for a given old path + * Same path is more specific, too, to allow updates to the same file. + * @param rootDir + * @param oldPath + * @param newPath + * @return true if more specific, false if not + */ + static final boolean isLongestMatchForPath(Path rootDir, Path oldPath, Path newPath) { + // Old and new path are the same -> "more" specific (update case) + if (oldPath.equals(newPath)) + return true; + + // Make pathes relative to config directory and count the "levels" (path depth) + // NOTE: we will never have a path containing "..", as our tree walkers are always inside this "root". + int oldPathDepth = rootDir.relativize(oldPath).getNameCount(); + int newPathDepth = rootDir.relativize(newPath).getNameCount(); + + // Check if this element has a higher path depth (longest match) + // Example: "foo.bar/test/one.txt" (depth 2) wins over "foo.bar.test.one.txt" (depth 0) + if (oldPathDepth < newPathDepth) + return true; + + // In case that both pathes have the same depth, we need to check on the position of dots. + // Example: /config/foo.bar/test/one.txt is less specific than /config/foo/bar.test/one.txt + if (oldPathDepth == newPathDepth) { + String oldPathS = oldPath.toString(); + String newPathS = newPath.toAbsolutePath().toString(); + int offset = 0; + while (offset > -1) { + if (newPathS.indexOf(".", offset) > oldPathS.indexOf(".", offset)) return true; + offset = oldPathS.indexOf(".", offset + 1); + } + } + + // All other cases: no, it's not more specific. + return false; + } + + /** + * Check path to be a directory, readable by us, but ignore if starting with a "." + * (as done for K8s secrets mounts). + * @param path + * @return true if suitable, false if not. + * @throws IOException when path cannot be resolved, maybe because of a broken symlink. + */ + public final static boolean isAptDir(Path path) throws IOException { + return path != null && Files.exists(path) && + Files.isDirectory(path) && Files.isReadable(path) && + !path.getFileName().toString().startsWith("."); + } + + /** + * Check if the file exists (follows symlinks), is a regular file (following symlinks), is readable (following symlinks), + * the filename does not start with a "." (not following the symlink!) and the file is less than 512 KB. + * @param path + * @return true if suitable file, false if not. + * @throws IOException when path cannot be accessed or resolved etc. + */ + public final static boolean isAptFile(Path path, BasicFileAttributes atts) throws IOException { + return path != null && Files.exists(path) && + Files.isRegularFile(path) && Files.isReadable(path) && + !path.getFileName().toString().startsWith(".") && + atts.size() < 512*1024; + } + + /** + * Parsing the relative path into a configuration property name. + * Using dots to mark scopes based on directories plus keeping dots used in the files name. + * @param path The path to the property file + * @return The property name + */ + public final static String parsePropertyNameFromPath(Path path, Path rootDir) { + // 1. get relative path based on the config dir ("/config"), + String property = ""; + if (! path.getParent().equals(rootDir)) + property += rootDir.relativize(path.getParent()).toString() + File.separatorChar; + // 2. add the file name (might be used for mangling in the future) + property += path.getFileName(); + // 3. replace all path seps with a ".", + property = property.replace(File.separatorChar, '.'); + // so "/config/foo/bar/test/one.txt" becomes "foo/bar/test/one.txt" becomes "foo.bar.test.one" property name + return property; + } + + /** + * Actually read the data from the file, assuming UTF-8 formatted content, creating properties from it. + * @param path The file to read + * @param mainAtts The files basic attributes like mod time, ... + * @return The configuration property this path represents + * @throws IOException When reading fails. + */ + static final DirProperty readPropertyFromPath(Path path, BasicFileAttributes mainAtts, Path rootPath) throws IOException { + if (Files.exists(path) && Files.isRegularFile(path) && Files.isReadable(path)) { + return new DirProperty( + new String(Files.readAllBytes(path), StandardCharsets.UTF_8), + mainAtts.lastModifiedTime(), + path.toAbsolutePath() + ); + } + throw new IOException("Cannot read property from '"+path.toString()+"'."); + } +} diff --git a/nucleus/payara-modules/nucleus-microprofile/config-service/src/main/java/fish/payara/nucleus/microprofile/config/source/PayaraConfigSource.java b/nucleus/payara-modules/nucleus-microprofile/config-service/src/main/java/fish/payara/nucleus/microprofile/config/source/PayaraConfigSource.java index c1f207aec90..c5794d5fd80 100644 --- a/nucleus/payara-modules/nucleus-microprofile/config-service/src/main/java/fish/payara/nucleus/microprofile/config/source/PayaraConfigSource.java +++ b/nucleus/payara-modules/nucleus-microprofile/config-service/src/main/java/fish/payara/nucleus/microprofile/config/source/PayaraConfigSource.java @@ -67,4 +67,12 @@ public PayaraConfigSource() { configService = null; } + /** + * Should only be used for test purposes + * @param configService Usually a mocked implementation + */ + PayaraConfigSource(ConfigProviderResolverImpl configService) { + this.domainConfiguration = null; + this.configService = configService; + } } diff --git a/nucleus/payara-modules/nucleus-microprofile/config-service/src/main/java/fish/payara/nucleus/microprofile/config/source/SecretsDirConfigSource.java b/nucleus/payara-modules/nucleus-microprofile/config-service/src/main/java/fish/payara/nucleus/microprofile/config/source/SecretsDirConfigSource.java deleted file mode 100644 index aee95511123..00000000000 --- a/nucleus/payara-modules/nucleus-microprofile/config-service/src/main/java/fish/payara/nucleus/microprofile/config/source/SecretsDirConfigSource.java +++ /dev/null @@ -1,183 +0,0 @@ -/* - * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER. - * - * Copyright (c) [2017-2020] Payara Foundation and/or its affiliates. All rights reserved. - * - * The contents of this file are subject to the terms of either the GNU - * General Public License Version 2 only ("GPL") or the Common Development - * and Distribution License("CDDL") (collectively, the "License"). You - * may not use this file except in compliance with the License. You can - * obtain a copy of the License at - * https://github.com/payara/Payara/blob/master/LICENSE.txt - * See the License for the specific - * language governing permissions and limitations under the License. - * - * When distributing the software, include this License Header Notice in each - * file and include the License file at glassfish/legal/LICENSE.txt. - * - * GPL Classpath Exception: - * The Payara Foundation designates this particular file as subject to the "Classpath" - * exception as provided by the Payara Foundation in the GPL Version 2 section of the License - * file that accompanied this code. - * - * Modifications: - * If applicable, add the following below the License Header, with the fields - * enclosed by brackets [] replaced by your own identifying information: - * "Portions Copyright [year] [name of copyright owner]" - * - * Contributor(s): - * If you wish your version of this file to be governed by only the CDDL or - * only the GPL Version 2, indicate your decision by adding "[Contributor] - * elects to include this software in this distribution under the [CDDL or GPL - * Version 2] license." If you don't indicate a single choice of license, a - * recipient has the option to distribute your version of this file under - * either the CDDL, the GPL Version 2 or to extend the choice of license to - * its licensees as provided above. However, if you add GPL Version 2 code - * and therefore, elected the GPL Version 2 license, then the option applies - * only if the new code is made subject to such option by the copyright - * holder. - */ -package fish.payara.nucleus.microprofile.config.source; - -import static java.util.Collections.unmodifiableMap; - -import java.io.File; -import java.io.IOException; -import java.nio.file.Files; -import java.nio.file.Path; -import java.nio.file.Paths; -import java.nio.file.attribute.FileTime; -import java.util.Map; -import java.util.Set; -import java.util.concurrent.ConcurrentHashMap; -import java.util.logging.Level; -import java.util.logging.Logger; -import org.eclipse.microprofile.config.spi.ConfigSource; - -/** - * Config Source that reads properties from files from a directory - * where filename is the property name and file contents is the property value. - * @since 4.1.181 - * @author steve - */ -public class SecretsDirConfigSource extends PayaraConfigSource implements ConfigSource { - - private Path secretsDir; - private ConcurrentHashMap properties; - private ConcurrentHashMap storedModifiedTimes; - - public SecretsDirConfigSource() { - findFile(); - loadProperties(); - } - - SecretsDirConfigSource(Path directory) { - super(true); - secretsDir = directory; - loadProperties(); - } - - @Override - public Map getProperties() { - return unmodifiableMap(properties); - } - - @Override - public Set getPropertyNames() { - return properties.keySet(); - } - - @Override - public int getOrdinal() { - return Integer.parseInt(configService.getMPConfig().getSecretDirOrdinality()); - } - - @Override - public String getValue(String property) { - String result = properties.get(property); - if (result != null) { - try { - // check the last modified time - FileTime ft = storedModifiedTimes.get(property); - Path path = Paths.get(secretsDir.toString(), property); - if (Files.exists(path) && Files.getLastModifiedTime(path).compareTo(ft) > 0) { - // file has been modified since last check - result = readFile(property); - storedModifiedTimes.put(property, Files.getLastModifiedTime(path)); - properties.put(property, result); - } - } catch (IOException ex) { - Logger.getLogger(SecretsDirConfigSource.class.getName()).log(Level.SEVERE, null, ex); - } - } else { - // check whether there is a file there now as there wasn't before - Path path = Paths.get(secretsDir.toString(), property); - if (Files.exists(path) && Files.isRegularFile(path) && Files.isReadable(path)) { - try { - result = readFile(property); - storedModifiedTimes.put(property, Files.getLastModifiedTime(path)); - properties.put(property, result); - } catch (IOException ex) { - Logger.getLogger(SecretsDirConfigSource.class.getName()).log(Level.SEVERE, null, ex); - } - } - } - return result; - } - - @Override - public String getName() { - return "Secrets Directory"; - } - - private void findFile() { - secretsDir = Paths.get(configService.getMPConfig().getSecretDir()); - - if (!Files.exists(secretsDir) || !Files.isDirectory(secretsDir) || !Files.isReadable(secretsDir)) { - // let's try it relative to server environment root - String instancePath = System.getProperty("com.sun.aas.instanceRoot"); - Path test = Paths.get(instancePath, secretsDir.toString()); - if (Files.exists(test) && Files.isDirectory(test) && Files.isReadable(test)) { - secretsDir = test; - } - } - } - - private String readFile(String name) { - String result = null; - if (Files.exists(secretsDir) && Files.isDirectory(secretsDir) && Files.isReadable(secretsDir)) { - try { - Path file = Paths.get(secretsDir.toString(), name); - if (Files.exists(file) && Files.isReadable(file)) { - StringBuilder collector = new StringBuilder(); - for (String line : Files.readAllLines(file)) { - collector.append(line); - } - result = collector.toString(); - } - } catch (IOException ex) { - Logger.getLogger(SecretsDirConfigSource.class.getName()).log(Level.SEVERE, null, ex); - } - } - return result; - } - - private void loadProperties() { - properties = new ConcurrentHashMap<>(); - storedModifiedTimes = new ConcurrentHashMap<>(); - if (Files.exists(secretsDir) && Files.isDirectory(secretsDir) && Files.isReadable(secretsDir)) { - File files[] = secretsDir.toFile().listFiles(); - for (File file : files) { - try { - if (file.isFile() && file.canRead()) { - properties.put(file.getName(), readFile(file.getName())); - storedModifiedTimes.put(file.getName(), Files.getLastModifiedTime(file.toPath())); - } - } catch (IOException ex) { - Logger.getLogger(SecretsDirConfigSource.class.getName()).log(Level.SEVERE, "Unable to read file in the directory", ex); - } - } - } - } - -} diff --git a/nucleus/payara-modules/nucleus-microprofile/config-service/src/main/java/fish/payara/nucleus/microprofile/config/spi/ConfigProviderResolverImpl.java b/nucleus/payara-modules/nucleus-microprofile/config-service/src/main/java/fish/payara/nucleus/microprofile/config/spi/ConfigProviderResolverImpl.java index 2ac8ed3da73..7e9312f92ce 100644 --- a/nucleus/payara-modules/nucleus-microprofile/config-service/src/main/java/fish/payara/nucleus/microprofile/config/spi/ConfigProviderResolverImpl.java +++ b/nucleus/payara-modules/nucleus-microprofile/config-service/src/main/java/fish/payara/nucleus/microprofile/config/spi/ConfigProviderResolverImpl.java @@ -63,6 +63,7 @@ import javax.inject.Inject; import javax.inject.Named; +import fish.payara.nucleus.executorservice.PayaraExecutorService; import org.eclipse.microprofile.config.Config; import org.eclipse.microprofile.config.spi.ConfigBuilder; import org.eclipse.microprofile.config.spi.ConfigProviderResolver; @@ -104,7 +105,7 @@ import fish.payara.nucleus.microprofile.config.source.PayaraExpressionConfigSource; import fish.payara.nucleus.microprofile.config.source.PayaraServerProperties; import fish.payara.nucleus.microprofile.config.source.PropertiesConfigSource; -import fish.payara.nucleus.microprofile.config.source.SecretsDirConfigSource; +import fish.payara.nucleus.microprofile.config.source.DirConfigSource; import fish.payara.nucleus.microprofile.config.source.ServerConfigSource; import fish.payara.nucleus.microprofile.config.source.SystemPropertyConfigSource; import fish.payara.nucleus.microprofile.config.source.extension.ExtensionConfigSourceService; @@ -134,6 +135,10 @@ public class ConfigProviderResolverImpl extends ConfigProviderResolver { @Inject private ServerContext context; + + // Some sources might want to execute background tasks in a controlled fashion + @Inject + private PayaraExecutorService executorService; // Gives access to deployed applications @Inject @@ -292,6 +297,10 @@ public ConfigBuilder getBuilder() { return new PayaraConfigBuilder(this); } + public PayaraExecutorService getExecutor() { + return this.executorService; + } + Config getNamedConfig(String applicationName) { Config result = null; ApplicationInfo info = applicationRegistry.get(applicationName); @@ -317,7 +326,7 @@ private List getDefaultSources(String appName, String moduleName) sources.add(new SystemPropertyConfigSource()); sources.add(new JNDIConfigSource()); sources.add(new PayaraServerProperties()); - sources.add(new SecretsDirConfigSource()); + sources.add(new DirConfigSource()); sources.add(new PasswordAliasConfigSource()); sources.add(new JDBCConfigSource()); if (appName != null) { diff --git a/nucleus/payara-modules/nucleus-microprofile/config-service/src/test/java/fish/payara/nucleus/microprofile/config/source/DirConfigSourceTest.java b/nucleus/payara-modules/nucleus-microprofile/config-service/src/test/java/fish/payara/nucleus/microprofile/config/source/DirConfigSourceTest.java new file mode 100644 index 00000000000..5300557f64c --- /dev/null +++ b/nucleus/payara-modules/nucleus-microprofile/config-service/src/test/java/fish/payara/nucleus/microprofile/config/source/DirConfigSourceTest.java @@ -0,0 +1,482 @@ +/* + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER. + * + * Copyright (c) 2017-2020 Payara Foundation and/or its affiliates. All rights reserved. + * + * The contents of this file are subject to the terms of either the GNU + * General Public License Version 2 only ("GPL") or the Common Development + * and Distribution License("CDDL") (collectively, the "License"). You + * may not use this file except in compliance with the License. You can + * obtain a copy of the License at + * https://github.com/payara/Payara/blob/master/LICENSE.txt + * See the License for the specific + * language governing permissions and limitations under the License. + * + * When distributing the software, include this License Header Notice in each + * file and include the License file at glassfish/legal/LICENSE.txt. + * + * GPL Classpath Exception: + * The Payara Foundation designates this particular file as subject to the "Classpath" + * exception as provided by the Payara Foundation in the GPL Version 2 section of the License + * file that accompanied this code. + * + * Modifications: + * If applicable, add the following below the License Header, with the fields + * enclosed by brackets [] replaced by your own identifying information: + * "Portions Copyright [year] [name of copyright owner]" + * + * Contributor(s): + * If you wish your version of this file to be governed by only the CDDL or + * only the GPL Version 2, indicate your decision by adding "[Contributor] + * elects to include this software in this distribution under the [CDDL or GPL + * Version 2] license." If you don't indicate a single choice of license, a + * recipient has the option to distribute your version of this file under + * either the CDDL, the GPL Version 2 or to extend the choice of license to + * its licensees as provided above. However, if you add GPL Version 2 code + * and therefore, elected the GPL Version 2 license, then the option applies + * only if the new code is made subject to such option by the copyright + * holder. + */ +package fish.payara.nucleus.microprofile.config.source; + +import fish.payara.nucleus.microprofile.config.spi.ConfigProviderResolverImpl; +import fish.payara.nucleus.microprofile.config.spi.MicroprofileConfigConfiguration; +import org.junit.After; +import org.junit.AfterClass; +import org.junit.BeforeClass; +import org.junit.Test; + +import java.io.File; +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.nio.file.attribute.BasicFileAttributes; +import java.nio.file.attribute.FileTime; +import java.time.Instant; +import java.util.Arrays; +import java.util.Collections; +import java.util.Comparator; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Random; +import java.util.concurrent.Executors; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.TimeUnit; + +import static java.lang.Boolean.FALSE; +import static java.lang.Boolean.TRUE; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +public class DirConfigSourceTest { + + private static Path testDirectory; + private static DirConfigSource source; + private static ScheduledExecutorService exec = Executors.newScheduledThreadPool(3); + private static ConfigProviderResolverImpl configService; + + @BeforeClass + public static void setUp() throws IOException { + testDirectory = Files.createTempDirectory("microprofile-config-test-"); + configService = mock(ConfigProviderResolverImpl.class); + // create & load + source = new DirConfigSource(testDirectory, configService); + } + + @AfterClass + public static void tearDown() throws IOException, InterruptedException { + exec.shutdown(); + exec.awaitTermination(1, TimeUnit.SECONDS); + Files.walk(testDirectory) + .sorted(Comparator.reverseOrder()) + .map(Path::toFile) + .forEach(File::delete); + } + + @After + public void cleanProps() { + // reset properties map after every test to avoid side effects + source.setProperties(Collections.emptyMap()); + } + + @Test + public void testFindDir_AbsolutePath() throws IOException { + // given + MicroprofileConfigConfiguration config = mock(MicroprofileConfigConfiguration.class); + when(configService.getMPConfig()).thenReturn(config); + when(config.getSecretDir()).thenReturn(testDirectory.toString()); + // when + Path sut = source.findDir(); + // then + assertEquals(testDirectory, sut); + } + + @Test + public void testFindDir_RelativePath() throws IOException { + // given + System.setProperty("com.sun.aas.instanceRoot", testDirectory.toString()); + MicroprofileConfigConfiguration config = mock(MicroprofileConfigConfiguration.class); + when(configService.getMPConfig()).thenReturn(config); + when(config.getSecretDir()).thenReturn("."); + + // when + Path sut = source.findDir(); + + // then + assertEquals(testDirectory, sut); + } + + @Test + public void testIsAptDir() throws IOException { + // given + Map examples = new HashMap<>(); + examples.put(subpath( "aptdir"), TRUE); + examples.put(subpath( ".unaptdir"), FALSE); + + // when & then + assertEquals(FALSE, DirConfigSource.isAptDir(null)); + assertEquals(FALSE, DirConfigSource.isAptDir(subpath( "aptnotexisting"))); + for (Map.Entry ex : examples.entrySet()) { + Files.createDirectories(ex.getKey()); + assertEquals(ex.getValue(), DirConfigSource.isAptDir(ex.getKey())); + } + } + + @Test + public void testIsAptFile() throws IOException { + + Map examples = new HashMap<>(); + examples.put(subpath( "aptdir", "aptfile"), TRUE); + examples.put(subpath( "aptdir", ".unaptfile"), FALSE); + + assertEquals(FALSE, DirConfigSource.isAptFile(null, null)); + assertEquals(FALSE, DirConfigSource.isAptFile(subpath( "aptdir", "aptnotexisting"), null)); + for (Map.Entry ex : examples.entrySet()) { + BasicFileAttributes atts = writeFile(ex.getKey(), "test"); + assertEquals(ex.getValue(), DirConfigSource.isAptFile(ex.getKey(), atts)); + } + + Path file100k = subpath("aptdir", "100k-file"); + BasicFileAttributes atts100k = writeRandFile(file100k, 100*1024); + assertEquals(TRUE, DirConfigSource.isAptFile(file100k, atts100k)); + + Path file600k = subpath("aptdir", "600k-file"); + BasicFileAttributes atts600k = writeRandFile(file600k, 600*1024); + assertEquals(FALSE, DirConfigSource.isAptFile(file600k, atts600k)); + } + + @Test + public void testParsePropertyNameFromPath() { + // given + Map examples = new HashMap<>(); + examples.put(subpath( "foo", "bar", "test", "ex"), "foo.bar.test.ex"); + examples.put(subpath( "foo.bar.test", "ex"), "foo.bar.test.ex"); + examples.put(subpath( "foo", "bar.test", "ex"), "foo.bar.test.ex"); + examples.put(subpath( "foo.bar", "test", "ex"), "foo.bar.test.ex"); + + // when & then + for (Map.Entry ex : examples.entrySet()) { + //System.out.println(ex.getKey()+" = "+ex.getValue()); + assertEquals(ex.getValue(), DirConfigSource.parsePropertyNameFromPath(ex.getKey(), testDirectory)); + } + } + + @Test + public void testReadPropertyFromPath() throws IOException { + // given + Path sut = subpath("aptdir", "sut-read-property"); + BasicFileAttributes attsSUT = writeFile(sut, "foobar"); + DirConfigSource.DirProperty example = new DirConfigSource.DirProperty( + "foobar", attsSUT.lastModifiedTime(), sut + ); + // when & then + assertEquals(example, DirConfigSource.readPropertyFromPath(sut, attsSUT, testDirectory)); + } + + @Test + public void testCheckLongestMatchForPath_SamePath() { + // given + List paths = Arrays.asList( + subpath("foo.bar.ex.test.hello"), + subpath("foo.bar.ex.test", "hello.txt"), + subpath("foo.bar.ex", "test", "hello.txt"), + subpath("foo.bar", "ex", "test", "hello.txt") + ); + + // when & then + for (Path p : paths) + assertTrue(DirConfigSource.isLongestMatchForPath(testDirectory, p, p)); + } + + @Test + public void testCheckLongestMatchForPath_PathDepthGrowing() { + // given + String propFile = "foo.bar.ex.test.hello"; + + for (int i = 0; i < propFile.chars().filter(ch -> ch == '.').count(); i++) { + // when + String newPath = propFile.replaceFirst("\\.", File.separator); + + // then + assertTrue(DirConfigSource.isLongestMatchForPath(testDirectory, subpath(propFile), subpath(newPath))); + assertFalse(DirConfigSource.isLongestMatchForPath(testDirectory, subpath(newPath), subpath(propFile))); + + propFile = newPath; + } + } + + @Test + public void testCheckLongestMatchForPath_PathDepthShrinking() { + // given + String propFile = Paths.get("foo", "bar", "ex", "test", "hello").toString(); + + for (int i = 0; i < propFile.chars().filter(ch -> ch == '.').count(); i++) { + // when + String newPath = propFile.replaceFirst(File.separator, "."); + + // then + assertFalse(DirConfigSource.isLongestMatchForPath(testDirectory, subpath(propFile), subpath(newPath))); + assertTrue(DirConfigSource.isLongestMatchForPath(testDirectory, subpath(newPath), subpath(propFile))); + + propFile = newPath; + } + } + + @Test + public void testCheckLongestMatchForPath_PathDepthEqualMoreSpecificDotsLeftGrowing() { + // given + String prop = "foo.bar.example.test.hello.world.stranger.ohmy.how.long.is.this"; + + // get all dot positions + int dotcount = (int)prop.chars().filter(ch -> ch == '.').count(); + int[] pos = new int[dotcount]; + int offset = 0; + for (int i = 0; i < pos.length; i++) { + pos[i] = prop.indexOf(".", offset); + offset = pos[i]+1; + } + + // move the slashes on the string and check for being more specific + for (int i = 0; i < dotcount-6; i++) { + // when + StringBuffer oldPath = new StringBuffer(prop) + .replace(pos[i], pos[i]+1, File.separator) + .replace(pos[i+2], pos[i+2]+1, File.separator) + .replace(pos[i+5], pos[i+5]+1, File.separator); + //System.out.println(oldPath.toString()); + StringBuffer newPath = new StringBuffer(prop) + .replace(pos[i+1], pos[i+1]+1, File.separator) + .replace(pos[i+3], pos[i+3]+1, File.separator) + .replace(pos[i+6], pos[i+6]+1, File.separator); + //System.out.println(newPath.toString()); + + // then + // --> we assert that independent on the number of dots in dir names (the total count of dots present + // does not change above), the path with a dot more counting from the left is always more specific. + // keep in mind that the number of subdirectory levels is not changing! + assertTrue(DirConfigSource.isLongestMatchForPath(testDirectory, subpath(oldPath.toString()), subpath(newPath.toString()))); + } + } + + @Test + public void testCheckLongestMatchForPath_PathDepthEqualMoreSpecificDotMoving() { + // given + List propPaths = Arrays.asList( + new String[]{"foo.bar", "example", "test", "hello"}, + new String[]{"foo", "bar.example", "test", "hello"}, + new String[]{"foo", "bar", "example.test", "hello"}, + new String[]{"foo", "bar", "example", "test.hello"} + ); + + // move the slashes on the string and check for being more specific + for (int i = 1; i < propPaths.size()-1; i++) { + // when & then + // --> we assert that a larger number of subdirectories before a dot is always a more specific path + assertTrue(DirConfigSource.isLongestMatchForPath(testDirectory, subpath(propPaths.get(i-1)), subpath(propPaths.get(i))));; + } + } + + @Test + public void testCheckLongestMatchForPath_PropNotPresent() { + // given + Map props = new HashMap<>(); + // a property with a most specific path + String property = "foo.bar.test.ex.one"; + props.put(property, + new DirConfigSource.DirProperty( + "test", FileTime.from(Instant.now()), + // different from call below, as we want to test a missing property! + subpath("foo.bar", "test.ex", "one"))); + source.setProperties(props); + + // when & then + assertTrue(source.isLongestMatchForPath("foo.bar.test.ex.two", subpath( "foo.bar", "test", "ex.two.txt"))); + } + + @Test + public void testRemovePropertyFromPath() { + // given + Map props = new HashMap<>(); + // a property with a most specific path + String property = "foo.bar.test"; + props.put(property, + new DirConfigSource.DirProperty( + "test", FileTime.from(Instant.now()), + subpath( "foo", "bar", "test"))); + source.setProperties(props); + assertEquals("test", source.getValue(property)); + + // when + source.removePropertyFromPath(subpath( "foo", "bar", "test")); + // then + assertTrue(source.getValue(property) == null); + + } + + @Test + public void testUpsertPropertyFromPath_InsertCase() throws IOException { + // given + Path sut = subpath("aptdir", "sut-upsert-property-insert"); + BasicFileAttributes attsSUT = writeFile(sut, "foobar"); + DirConfigSource.DirProperty example = new DirConfigSource.DirProperty( + "foobar", attsSUT.lastModifiedTime(), sut); + + // when & then + assertEquals(true, source.upsertPropertyFromPath(sut, attsSUT)); + assertEquals(example.propertyValue, source.getValue("aptdir.sut-upsert-property-insert")); + } + + @Test + public void testUpsertPropertyFromPath_UpdateCase() throws IOException { + // given + Path sut = subpath("aptdir", "sut-upsert-property-update"); + BasicFileAttributes attsSUT = writeFile(sut, "foobar"); + DirConfigSource.DirProperty example = new DirConfigSource.DirProperty( + "foobar", attsSUT.lastModifiedTime(), sut); + + assertEquals(true, source.upsertPropertyFromPath(sut, attsSUT)); + assertEquals(example.propertyValue, source.getValue("aptdir.sut-upsert-property-update")); + + // when & then + BasicFileAttributes attsUpdate = writeFile(sut, "foobar2"); + assertEquals(true, source.upsertPropertyFromPath(sut, attsUpdate)); + assertEquals("foobar2", source.getValue("aptdir.sut-upsert-property-update")); + } + + @Test + public void testPropertyWatcher_RegisterAndInit() throws Exception { + // given + Map examples = new HashMap<>(); + examples.put(subpath( "init-watcher", "foo", "bar", "test", "ex"), "init-watcher.foo.bar.test.ex"); + examples.put(subpath( "init-watcher", "foo.bar.test", "hello"), "init-watcher.foo.bar.test.hello"); + examples.put(subpath( "init-watcher", ".foo", "ex"), "init-watcher.foo.ex"); + for (Map.Entry ex : examples.entrySet()) { + writeFile(ex.getKey(), "foobar"); + } + Files.createSymbolicLink(subpath( "init-watcher", "foo.hello"), subpath("init-watcher", ".foo", "ex")); + + // when + DirConfigSource.DirPropertyWatcher watcher = source.createWatcher(subpath("init-watcher")); + + // then + assertEquals("foobar", source.getValue("init-watcher.foo.bar.test.ex")); + assertEquals("foobar", source.getValue("init-watcher.foo.bar.test.hello")); + assertEquals(null, source.getValue("init-watcher.foo.ex")); + assertEquals("foobar", source.getValue("init-watcher.foo.hello")); + } + + @Test + public void testPropertyWatcher_RunFilesNewUpdate() throws Exception { + // given + writeFile(subpath("watcher-files", "foobar"), "test"); + writeFile(subpath("watcher-files", ".hidden", "foobar"), "hidden"); + Files.createSymbolicLink(subpath("watcher-files", "revealed"), subpath("watcher-files", ".hidden", "foobar")); + + DirConfigSource.DirPropertyWatcher watcher = source.createWatcher(subpath("watcher-files")); + exec.scheduleWithFixedDelay(watcher, 0, 10, TimeUnit.MILLISECONDS); + + assertEquals("test", source.getValue("watcher-files.foobar")); + assertEquals("hidden", source.getValue("watcher-files.revealed")); + + // when + writeFile(subpath("watcher-files", "foobar"), "test2"); + writeFile(subpath("watcher-files", "example"), "test2"); + writeFile(subpath("watcher-files", "reveal", ".hidden"), "test2"); + writeFile(subpath("watcher-files", ".hidden", "foobar"), "showme"); + Files.delete(subpath("watcher-files", "revealed")); + Files.createSymbolicLink(subpath("watcher-files", "revealed"), subpath("watcher-files", ".hidden", "foobar")); + Thread.sleep(100); + + // then + assertEquals("test2", source.getValue("watcher-files.foobar")); + assertEquals("test2", source.getValue("watcher-files.example")); + assertEquals("showme", source.getValue("watcher-files.revealed")); + assertEquals(null, source.getValue("watcher-files.reveal.hidden")); + } + + @Test + public void testPropertyWatcher_RunNewDir() throws Exception { + // given + writeFile(subpath("watcher-newdir", "test"), "test"); + + DirConfigSource.DirPropertyWatcher watcher = source.createWatcher(subpath("watcher-newdir")); + exec.scheduleWithFixedDelay(watcher, 0, 10, TimeUnit.MILLISECONDS); + + assertEquals("test", source.getValue("watcher-newdir.test")); + + // when + writeFile(subpath("watcher-newdir", "foobar/test"), "test"); + writeFile(subpath("watcher-newdir", ".hidden/foobar"), "test"); + Thread.sleep(100); + + // then + assertEquals("test", source.getValue("watcher-newdir.foobar.test")); + assertEquals(null, source.getValue("watcher-newdir.hidden.foobar")); + } + + @Test + public void testPropertyWatcher_RunRemove() throws Exception { + // given + writeFile(subpath("watcher-remove", "test"), "test"); + + DirConfigSource.DirPropertyWatcher watcher = source.createWatcher(subpath("watcher-remove")); + exec.scheduleWithFixedDelay(watcher, 0, 10, TimeUnit.MILLISECONDS); + + assertEquals("test", source.getValue("watcher-remove.test")); + + // when + Files.delete(subpath("watcher-remove", "test")); + Thread.sleep(100); + + // then + assertEquals(null, source.getValue("watcher-remove.test")); + } + + public static BasicFileAttributes writeFile(Path filepath, String content) throws IOException { + Files.createDirectories(filepath.getParent()); + Files.write(filepath, content.getBytes(StandardCharsets.UTF_8)); + return Files.readAttributes(filepath, BasicFileAttributes.class); + } + + public static BasicFileAttributes writeRandFile(Path filepath, int bytes) throws IOException { + Files.createDirectories(filepath.getParent()); + + Random rnd = new Random(); + byte[] content = new byte[bytes]; + rnd.nextBytes(content); + + Files.write(filepath, content); + return Files.readAttributes(filepath, BasicFileAttributes.class); + } + + private static Path subpath(String... subpath) { + return Paths.get(testDirectory.toString(), subpath); + } + +} diff --git a/nucleus/payara-modules/nucleus-microprofile/config-service/src/test/java/fish/payara/nucleus/microprofile/config/source/NewTestSuite.java b/nucleus/payara-modules/nucleus-microprofile/config-service/src/test/java/fish/payara/nucleus/microprofile/config/source/NewTestSuite.java index 82983cc8e55..84af88c0341 100644 --- a/nucleus/payara-modules/nucleus-microprofile/config-service/src/test/java/fish/payara/nucleus/microprofile/config/source/NewTestSuite.java +++ b/nucleus/payara-modules/nucleus-microprofile/config-service/src/test/java/fish/payara/nucleus/microprofile/config/source/NewTestSuite.java @@ -47,7 +47,7 @@ * @author steve */ @RunWith(Suite.class) -@Suite.SuiteClasses({ SecretsDirConfigSourceTest.class }) +@Suite.SuiteClasses({ DirConfigSourceTest.class }) public class NewTestSuite { diff --git a/nucleus/payara-modules/nucleus-microprofile/config-service/src/test/java/fish/payara/nucleus/microprofile/config/source/SecretsDirConfigSourceTest.java b/nucleus/payara-modules/nucleus-microprofile/config-service/src/test/java/fish/payara/nucleus/microprofile/config/source/SecretsDirConfigSourceTest.java deleted file mode 100644 index b92979d7041..00000000000 --- a/nucleus/payara-modules/nucleus-microprofile/config-service/src/test/java/fish/payara/nucleus/microprofile/config/source/SecretsDirConfigSourceTest.java +++ /dev/null @@ -1,167 +0,0 @@ -/* - * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER. - * - * Copyright (c) 2017-2020 Payara Foundation and/or its affiliates. All rights reserved. - * - * The contents of this file are subject to the terms of either the GNU - * General Public License Version 2 only ("GPL") or the Common Development - * and Distribution License("CDDL") (collectively, the "License"). You - * may not use this file except in compliance with the License. You can - * obtain a copy of the License at - * https://github.com/payara/Payara/blob/master/LICENSE.txt - * See the License for the specific - * language governing permissions and limitations under the License. - * - * When distributing the software, include this License Header Notice in each - * file and include the License file at glassfish/legal/LICENSE.txt. - * - * GPL Classpath Exception: - * The Payara Foundation designates this particular file as subject to the "Classpath" - * exception as provided by the Payara Foundation in the GPL Version 2 section of the License - * file that accompanied this code. - * - * Modifications: - * If applicable, add the following below the License Header, with the fields - * enclosed by brackets [] replaced by your own identifying information: - * "Portions Copyright [year] [name of copyright owner]" - * - * Contributor(s): - * If you wish your version of this file to be governed by only the CDDL or - * only the GPL Version 2, indicate your decision by adding "[Contributor] - * elects to include this software in this distribution under the [CDDL or GPL - * Version 2] license." If you don't indicate a single choice of license, a - * recipient has the option to distribute your version of this file under - * either the CDDL, the GPL Version 2 or to extend the choice of license to - * its licensees as provided above. However, if you add GPL Version 2 code - * and therefore, elected the GPL Version 2 license, then the option applies - * only if the new code is made subject to such option by the copyright - * holder. - */ -package fish.payara.nucleus.microprofile.config.source; - -import java.io.File; -import java.io.IOException; -import java.nio.file.Files; -import java.nio.file.Path; -import java.nio.file.Paths; -import java.nio.file.attribute.FileTime; -import java.util.HashMap; -import java.util.HashSet; -import java.util.Map; -import java.util.concurrent.TimeUnit; - -import org.junit.After; -import org.junit.Before; -import org.junit.Test; - -import static java.util.Arrays.asList; -import static org.junit.Assert.*; - -/** - * - * @author steve - */ -public class SecretsDirConfigSourceTest { - - private Path testDirectory; - private SecretsDirConfigSource source; - - @Before - public void setUp() throws IOException { - testDirectory = Files.createTempDirectory("microprofile-config-test"); - // create a couple of test files - Path file1 = Paths.get(testDirectory.toString(), "property1"); - Path file2 = Paths.get(testDirectory.toString(), "property2"); - file1 = Files.createFile(file1); - Files.write(file1, "value1".getBytes()); - file2 = Files.createFile(file2); - Files.write(file2, "value2".getBytes()); - source = new SecretsDirConfigSource(testDirectory); - } - - @After - public void tearDown() throws IOException { - for (File file : testDirectory.toFile().listFiles()) { - file.delete(); - } - Files.delete(testDirectory); - } - - /** - * Test of getProperties method, of class SecretsDirConfigSource. - */ - @Test - public void testGetProperties() { - Map expected = new HashMap<>(); - expected.put("property1", "value1"); - expected.put("property2", "value2"); - assertEquals(expected, source.getProperties()); - } - - /** - * Test of getPropertyNames method, of class SecretsDirConfigSource. - */ - @Test - public void testGetPropertyNames() { - assertEquals(new HashSet<>(asList("property1", "property2")), source.getPropertyNames()); - } - - /** - * Test of getValue method, of class SecretsDirConfigSource. - */ - @Test - public void testGetValue() { - assertEquals("value1", source.getValue("property1")); - assertEquals("value2", source.getValue("property2")); - } - - /** - * Test of getName method, of class SecretsDirConfigSource. - */ - @Test - public void testGetName() { - assertEquals("Secrets Directory", source.getName()); - } - - /** - * Test the changed Property - * @throws java.io.IOException - */ - @Test - public void testChangeProperty() throws IOException { - assertEquals("value1", source.getValue("property1")); - // change the file - Path file1 = Paths.get(testDirectory.toString(), "property1"); - Files.write(file1, "value-changed".getBytes()); - try { - FileTime nowplus1sec = FileTime.fromMillis(System.currentTimeMillis() + TimeUnit.SECONDS.toMillis(1)); - Files.setLastModifiedTime(file1, nowplus1sec); - assertEquals("value-changed", source.getValue("property1")); - } finally { - // clean up - Files.write(file1, "value1".getBytes()); - } - } - - /** - * Tests getting a new property as the file has now appeared - */ - @Test - public void testNewFile() throws IOException { - assertNull(source.getValue("property-new")); - // change the file - Path file1 = Paths.get(testDirectory.toString(), "property-new"); - Files.write(file1, "newValue".getBytes()); - try { - assertEquals("newValue", source.getValue("property-new")); - } finally { - // clean up - Files.delete(file1); - } - } - - @Test - public void testBadDirectoryNoBlowUp() { - assertNull(new SecretsDirConfigSource(Paths.get(testDirectory.toString(), "FOOBLE")).getValue("BILLY")); - } -}