diff --git a/benchmark/build.gradle b/benchmark/build.gradle index be94c33..b975d94 100644 --- a/benchmark/build.gradle +++ b/benchmark/build.gradle @@ -1,9 +1,9 @@ plugins { - id "io.morethan.jmhreport" version "0.9.0" - id "org.cyclonedx.bom" version "1.7.2" - id "com.github.spotbugs" version "5.0.13" id "checkstyle" id "pmd" + id "io.morethan.jmhreport" version "0.9.0" + id "org.cyclonedx.bom" version "1.7.4" + id "com.github.spotbugs" version "6.0.0-beta.3" } apply from: rootProject.file('gradle/test/jmh.gradle') diff --git a/build.gradle b/build.gradle index 024a5ba..eaa832a 100644 --- a/build.gradle +++ b/build.gradle @@ -31,7 +31,7 @@ ext { } subprojects { - apply from: rootProject.file('gradle/ide/idea.gradle') + //apply from: rootProject.file('gradle/ide/idea.gradle') apply from: rootProject.file('gradle/repositories/maven.gradle') apply from: rootProject.file('gradle/compile/java.gradle') apply from: rootProject.file('gradle/test/junit5.gradle') diff --git a/config/build.gradle b/config/build.gradle new file mode 100644 index 0000000..7e4a80d --- /dev/null +++ b/config/build.gradle @@ -0,0 +1,5 @@ +dependencies { + api project(':settings-datastructures') + testImplementation project(':settings-datastructures-json') + testImplementation project(':settings-datastructures-yaml') +} diff --git a/config/gradle.properties b/config/gradle.properties new file mode 100644 index 0000000..8d6d281 --- /dev/null +++ b/config/gradle.properties @@ -0,0 +1 @@ +version = 5.0.5 \ No newline at end of file diff --git a/config/src/main/java/module-info.java b/config/src/main/java/module-info.java new file mode 100644 index 0000000..d365de4 --- /dev/null +++ b/config/src/main/java/module-info.java @@ -0,0 +1,13 @@ +import org.xbib.config.ConfigLogger; +import org.xbib.config.NullConfigLogger; +import org.xbib.config.SystemConfigLogger; +import org.xbib.settings.SettingsLoader; + +module org.xbib.config { + exports org.xbib.config; + uses ConfigLogger; + uses SettingsLoader; + provides ConfigLogger with NullConfigLogger, SystemConfigLogger; + requires org.xbib.settings.api; + requires transitive org.xbib.settings.datastructures; +} diff --git a/config/src/main/java/org/xbib/config/ConfigException.java b/config/src/main/java/org/xbib/config/ConfigException.java new file mode 100644 index 0000000..d089fa0 --- /dev/null +++ b/config/src/main/java/org/xbib/config/ConfigException.java @@ -0,0 +1,17 @@ +package org.xbib.config; + +@SuppressWarnings("serial") +public class ConfigException extends RuntimeException { + + public ConfigException(Exception e) { + super(e); + } + + public ConfigException(String message) { + super(message); + } + + public ConfigException(String message, Throwable cause) { + super(message, cause); + } +} diff --git a/config/src/main/java/org/xbib/config/ConfigFinder.java b/config/src/main/java/org/xbib/config/ConfigFinder.java new file mode 100644 index 0000000..a3cb674 --- /dev/null +++ b/config/src/main/java/org/xbib/config/ConfigFinder.java @@ -0,0 +1,168 @@ +package org.xbib.config; + +import java.io.IOException; +import java.nio.file.FileSystem; +import java.nio.file.FileSystems; +import java.nio.file.FileVisitOption; +import java.nio.file.FileVisitResult; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.PathMatcher; +import java.nio.file.SimpleFileVisitor; +import java.nio.file.attribute.BasicFileAttributes; +import java.nio.file.attribute.FileTime; +import java.util.ArrayList; +import java.util.Collections; +import java.util.Comparator; +import java.util.EnumSet; +import java.util.List; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +public class ConfigFinder { + + private final FileSystem fileSystem; + + private final EnumSet opts; + + private final List result; + + private Comparator comparator; + + public ConfigFinder() { + this(FileSystems.getDefault(), EnumSet.of(FileVisitOption.FOLLOW_LINKS)); + } + + public ConfigFinder(FileSystem fileSystem, EnumSet opts) { + this.fileSystem = fileSystem; + this.opts = opts; + this.result = new ArrayList<>(); + } + + public ConfigFinder find(String path, String pathPattern) throws IOException { + return find(null, null, fileSystem.getPath(path), pathPattern); + } + + public ConfigFinder find(String base, String basePattern, String path, String pathPattern) throws IOException { + return find(base == null || base.isEmpty() ? null : fileSystem.getPath(base), basePattern, + path == null || path.isEmpty() ? null : fileSystem.getPath(path), pathPattern); + } + + public ConfigFinder find(Path base, String basePattern, Path path, String pathPattern) throws IOException { + return find(base, basePattern, path, pathPattern, null); + } + + /** + * Find the most recent version of a file. + * + * @param base the path of the base directory + * @param basePattern a pattern to match directory entries in the base directory or null to match '*' + * @param path the path of the file if no recent path can be found in the base directory or null + * @param pathPattern the file name pattern to match + * @param modifiedSince time stamp for file or null + * @return this Finder + * @throws IOException if find fails + */ + public ConfigFinder find(Path base, + String basePattern, + Path path, + String pathPattern, + FileTime modifiedSince) throws IOException { + if (base != null && path == null) { + // find input in base + final PathMatcher baseMatcher = base.getFileSystem() + .getPathMatcher("glob:" + (basePattern != null ? basePattern : "*")); + final PathMatcher pathMatcher = base.getFileSystem() + .getPathMatcher("glob:" + (pathPattern != null ? pathPattern : "*")); + List directories = new ArrayList<>(); + List list = Files.find(base, 1, + (p, a) -> { + if (Files.isDirectory(p) && baseMatcher.matches(p.getFileName())) { + directories.add(p); + return false; + } + return Files.isRegularFile(p) && pathMatcher.matches(p.getFileName()); + }, FileVisitOption.FOLLOW_LINKS) + .collect(Collectors.toList()); + if (directories.isEmpty()) { + return this; + } + list.sort(LAST_MODIFIED_COMPARATOR.reversed()); + result.addAll(list); + path = list.iterator().next(); + } + if (path == null) { + return this; + } + final PathMatcher pathMatcher = path.getFileSystem() + .getPathMatcher("glob:" + (pathPattern != null ? pathPattern : "*")); + Files.walkFileTree(path, opts, Integer.MAX_VALUE, new SimpleFileVisitor<>() { + @Override + public FileVisitResult visitFile(Path p, BasicFileAttributes a) { + if ((Files.isRegularFile(p) && pathMatcher.matches(p.getFileName())) && + (modifiedSince == null || a.lastModifiedTime().toMillis() > modifiedSince.toMillis())) { + result.add(p); + } + return FileVisitResult.CONTINUE; + } + }); + return this; + } + + public ConfigFinder sortBy(String mode) { + if ("lastmodified".equals(mode)) { + this.comparator = LAST_MODIFIED_COMPARATOR; + } else if ("name".equals(mode)) { + this.comparator = PATH_NAME_COMPARATOR; + } + return this; + } + + public ConfigFinder order(String mode) { + if ("desc".equals(mode)) { + this.comparator = Collections.reverseOrder(comparator); + } + return this; + } + + public Stream getPathFiles() { + return getPathFiles(-1); + } + + public Stream getPathFiles(long max) { + if (comparator != null) { + result.sort(comparator); + } + return result.stream().limit(max < 0 ? result.size() : max); + } + + public Stream skipPathFiles(long skip) { + if (comparator != null) { + result.sort(comparator); + } + return result.stream().skip(skip < 0 ? 0 : skip); + } + + public Stream getPaths() { + return getPaths(-1); + } + + public Stream getPaths(long max) { + if (comparator != null) { + result.sort(comparator); + } + return result.stream() + .map(p -> p.toAbsolutePath().toString()) + .limit(max < 0 ? result.size() : max); + } + + private static final Comparator LAST_MODIFIED_COMPARATOR = Comparator.comparing(p -> { + try { + return Files.getLastModifiedTime(p); + } catch (IOException e) { + return null; + } + }); + + private static final Comparator PATH_NAME_COMPARATOR = Comparator.comparing(Path::toString); +} diff --git a/config/src/main/java/org/xbib/config/ConfigLoader.java b/config/src/main/java/org/xbib/config/ConfigLoader.java new file mode 100644 index 0000000..6505a07 --- /dev/null +++ b/config/src/main/java/org/xbib/config/ConfigLoader.java @@ -0,0 +1,311 @@ +package org.xbib.config; + +import java.io.BufferedReader; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.io.Reader; +import java.io.StringReader; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.sql.SQLException; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Locale; +import java.util.Map; +import java.util.Optional; +import java.util.ServiceLoader; +import java.util.stream.Collectors; +import org.xbib.settings.Settings; +import org.xbib.settings.SettingsBuilder; +import org.xbib.settings.SettingsLoader; +import org.xbib.settings.SettingsLoaderService; + +/** + * A configuration loader for configuration files. + */ +public class ConfigLoader { + + private static final Map map = new HashMap<>(); + + private ConfigLogger logger; + + private ConfigLoader() { + } + + private static class Holder { + private static ConfigLogger createConfigLogger() { + ServiceLoader serviceLoader = ServiceLoader.load(ConfigLogger.class); + Optional optionalConfigLogger = serviceLoader.findFirst(); + return optionalConfigLogger.orElse(new NullConfigLogger()); + } + + private static final ConfigLoader configLoader = new ConfigLoader().withLogger(createConfigLogger()); + } + + public static ConfigLoader getInstance() { + return Holder.configLoader; + } + + public ConfigLoader withLogger(ConfigLogger logger) { + this.logger = logger; + return this; + } + + public synchronized Settings load(ConfigParams configParams) throws ConfigException { + map.computeIfAbsent(configParams, p -> internalLoad(p) + .replacePropertyPlaceholders() + .build()); + return map.get(configParams); + } + + private SettingsBuilder internalLoad(ConfigParams params) throws ConfigException { + SettingsBuilder settings = Settings.settingsBuilder(); + if (params.withSystemEnvironment) { + settings.loadFromSystemEnvironment(); + } + if (params.withSystemProperties) { + settings.loadFromSystemProperties(); + } + if (!params.settings.isEmpty()) { + for (Settings s : params.settings) { + settings.put(s); + } + } + if (!params.reader.isEmpty()) { + for (ConfigParams.SuffixedReader reader : params.reader) { + SettingsBuilder readerSettings = createSettingsFromReader(reader.reader, reader.suffix); + if (readerSettings != null) { + settings.put(readerSettings.build()); + if (!params.includeAll) { + return overrideFromProperties(params, settings); + } + } + } + } + if (params.args != null) { + SettingsBuilder argsSettings = createSettingsFromArgs(params); + if (argsSettings != null) { + settings.put(argsSettings.build()); + if (!params.includeAll) { + return overrideFromProperties(params, settings); + } + } + } + if (params.withStdin) { + SettingsBuilder stdinSettings = createSettingsFromStdin(); + if (stdinSettings != null) { + settings.put(stdinSettings.build()); + if (!params.includeAll) { + return overrideFromProperties(params, settings); + } + } + } + if (!params.fileLocations.isEmpty()) { + SettingsBuilder fileSettings = createSettingsFromFile(params.fileLocations); + if (fileSettings != null) { + settings.put(fileSettings.build()); + if (!params.includeAll) { + return overrideFromProperties(params, settings); + } + } + } + if (!params.fileNamesWithoutSuffix.isEmpty()) { + for (String fileNameWithoutSuffix : params.fileNamesWithoutSuffix) { + SettingsBuilder fileSettings = createSettingsFromFile(createListOfLocations(params, fileNameWithoutSuffix)); + if (fileSettings != null) { + settings.put(fileSettings.build()); + if (!params.includeAll) { + return overrideFromProperties(params, settings); + } + } + } + for (String fileNameWithoutSuffix : params.fileNamesWithoutSuffix) { + if (params.classLoaders != null) { + for (ClassLoader cl : params.classLoaders) { + if (cl != null) { + SettingsBuilder classpathSettings = createClasspathSettings(params, cl, fileNameWithoutSuffix); + if (classpathSettings != null) { + settings.put(classpathSettings.build()); + if (!params.includeAll) { + return overrideFromProperties(params, settings); + } + } + } + } + } + } + } + if (!params.jdbcLookups.isEmpty()) { + for (ConfigParams.JdbcLookup jdbcLookup : params.jdbcLookups) { + try { + settings.fromJdbc(jdbcLookup.connection, jdbcLookup.statement, jdbcLookup.params); + } catch (SQLException sqlException) { + throw new ConfigException(sqlException); + } + } + } + if (params.includeAll) { + return overrideFromProperties(params, settings); + } + if (params.failIfEmpty) { + throw new ConfigException("no config found"); + } + return settings; + } + + private SettingsBuilder createSettingsFromArgs(ConfigParams params) throws ConfigException { + if (!params.fileNamesWithoutSuffix.isEmpty() && params.args != null) { + for (String fileNameWithoutSuffix : params.fileNamesWithoutSuffix) { + for (String suffix : SettingsLoaderService.getInstance().getSuffixes()) { + for (int i = 0; i < params.args.size() - 1; i++) { + String arg = params.args.get(i); + String s = params.directoryName != null ? + "--" + params.directoryName + "-" + fileNameWithoutSuffix + "." + suffix : + "--" + fileNameWithoutSuffix + "." + suffix; + if (arg.equals(s)) { + return createSettingsFromReader(new StringReader(params.args.get(i + 1)), suffix); + } + } + } + } + } + return null; + } + + private SettingsBuilder createSettingsFromStdin() throws ConfigException { + if (System.in != null) { + try { + int numBytesWaiting = System.in.available(); + if (numBytesWaiting > 0) { + String suffix = System.getProperty("config.format", "yaml"); + return createSettingsFromStream(System.in, "." + suffix); + } + } catch (IOException e) { + throw new ConfigException(e); + } + } + return null; + } + + private SettingsBuilder createSettingsFromFile(List settingsFileNames) throws ConfigException { + SettingsBuilder settings = Settings.settingsBuilder(); + for (String settingsFileName: settingsFileNames) { + int pos = settingsFileName.lastIndexOf('.'); + String suffix = (pos > 0 ? settingsFileName.substring(pos + 1) : "").toLowerCase(Locale.ROOT); + Path path = Paths.get(settingsFileName); + if (logger != null) { + logger.info("trying " + path); + } + if (Files.exists(path)) { + if (logger != null) { + logger.info("found path: " + path); + } + System.setProperty("config.path", path.getParent().toString()); + try { + InputStream inputStream = Files.newInputStream(path); + SettingsBuilder fileSettings = createSettingsFromStream(inputStream, suffix); + if (fileSettings != null) { + settings.put(fileSettings.build()); + } + } catch (Exception e) { + throw new ConfigException(e); + } + } + } + return settings.isEmpty() ? null : settings; + } + + private SettingsBuilder createClasspathSettings(ConfigParams params, + ClassLoader classLoader, + String fileNameWithoutSuffix) throws ConfigException { + SettingsBuilder settings = Settings.settingsBuilder(); + for (String suffix : SettingsLoaderService.getInstance().getSuffixes()) { + String path = params.directoryName != null ? + params.directoryName + '-' + fileNameWithoutSuffix + suffix : fileNameWithoutSuffix + suffix; + InputStream inputStream = classLoader.getResourceAsStream(path); + if (inputStream != null) { + if (logger != null) { + logger.info("found resource: " + path); + } + SettingsBuilder streamSettings = createSettingsFromStream(inputStream, suffix); + if (streamSettings != null) { + settings.put(streamSettings.build()); + } + } + } + return settings.isEmpty() ? null : settings; + } + + private SettingsBuilder createSettingsFromStream(InputStream inputStream, + String suffix) throws ConfigException { + if (inputStream == null) { + if (logger != null) { + logger.error("unable to open input stream"); + } + return null; + } + return createSettingsFromReader(new InputStreamReader(inputStream, StandardCharsets.UTF_8), suffix); + } + + private SettingsBuilder createSettingsFromReader(Reader reader, + String suffix) throws ConfigException { + if (reader == null) { + if (logger != null) { + logger.error("unable to open reader"); + } + return null; + } + SettingsLoader settingsLoader = SettingsLoaderService.getInstance().loaderFromResource(suffix); + if (settingsLoader != null) { + SettingsBuilder settings; + try (BufferedReader bufferedReader = new BufferedReader(reader)) { + String content = bufferedReader.lines().collect(Collectors.joining("\n")); + settings = Settings.settingsBuilder().put(settingsLoader.load(content)); + } catch (IOException e) { + throw new ConfigException(e); + } + return settings; + } else { + if (logger != null) { + logger.error("suffix is invalid: " + suffix); + } + } + return null; + } + + private SettingsBuilder overrideFromProperties(ConfigParams params, SettingsBuilder settingsBuilder) { + if (params.withSystemPropertiesOverride) { + settingsBuilder.map(e -> { + String key = e.getKey(); + String value = System.getProperty(params.directoryName != null ? params.directoryName + '.' + key : key); + return value != null ? Map.entry(key, value) : Map.entry(key, e.getValue()); + }); + } + return settingsBuilder; + } + + private List createListOfLocations(ConfigParams params, + String fileNameWithoutSuffix) { + List list = new ArrayList<>(); + for (String suffix : SettingsLoaderService.getInstance().getSuffixes()) { + String xdgConfigHome = System.getenv("XDG_CONFIG_HOME"); + if (xdgConfigHome == null) { + xdgConfigHome = System.getProperty("user.home") + "/.config"; + } + if (params.directoryName != null) { + list.add(params.directoryName + '-' + fileNameWithoutSuffix + "." + suffix); + list.add(xdgConfigHome + '/' + params.directoryName + '/' + fileNameWithoutSuffix + "." + suffix); + list.add("/etc/" + params.directoryName + '/' + fileNameWithoutSuffix + "." + suffix); + } else { + list.add(fileNameWithoutSuffix + "." + suffix); + list.add(xdgConfigHome + '/' + fileNameWithoutSuffix + "." + suffix); + list.add("/etc/" + fileNameWithoutSuffix + "." + suffix); + } + } + return list; + } +} diff --git a/config/src/main/java/org/xbib/config/ConfigLogger.java b/config/src/main/java/org/xbib/config/ConfigLogger.java new file mode 100644 index 0000000..ca600db --- /dev/null +++ b/config/src/main/java/org/xbib/config/ConfigLogger.java @@ -0,0 +1,10 @@ +package org.xbib.config; + +public interface ConfigLogger { + + void info(String string); + + void warn(String string); + + void error(String message); +} diff --git a/config/src/main/java/org/xbib/config/ConfigParams.java b/config/src/main/java/org/xbib/config/ConfigParams.java new file mode 100644 index 0000000..2caa363 --- /dev/null +++ b/config/src/main/java/org/xbib/config/ConfigParams.java @@ -0,0 +1,192 @@ +package org.xbib.config; + +import java.io.IOException; +import java.io.Reader; +import java.sql.Connection; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Comparator; +import java.util.List; +import java.util.Objects; +import org.xbib.settings.Settings; +import org.xbib.settings.datastructures.DatastructureSettings; + +public class ConfigParams implements Comparable { + + private static final Comparator COMPARATOR = + Comparator.comparing(ConfigParams::toString); + + boolean withSystemEnvironment = false; + + boolean withSystemProperties = false; + + boolean failIfEmpty = false; + + boolean includeAll = false; + + boolean withStdin = false; + + boolean withSystemPropertiesOverride = false; + + List classLoaders = null; + + final List reader = new ArrayList<>(); + + final List jdbcLookups = new ArrayList<>(); + + final List settings = new ArrayList<>(); + + List args = null; + + String directoryName = null; + + final List fileNamesWithoutSuffix = new ArrayList<>(); + + final List fileLocations = new ArrayList<>(); + + public ConfigParams() { + } + + public ConfigParams withSystemEnvironment() { + this.withSystemEnvironment = true; + return this; + } + + public ConfigParams withSystemProperties() { + this.withSystemProperties = true; + return this; + } + + public ConfigParams withSystemPropertiesOverride() { + this.withSystemPropertiesOverride = true; + return this; + } + + public ConfigParams includeAll() { + this.includeAll = true; + return this; + } + + public ConfigParams failIfEmpty() { + this.failIfEmpty = true; + return this; + } + + public ConfigParams withStdin(boolean withStdin) { + this.withStdin = withStdin; + return this; + } + + public ConfigParams withArgs(String[] args) { + this.args = Arrays.asList(args); + return this; + } + + public ConfigParams withClassLoaders(ClassLoader... classLoaders) { + this.classLoaders = Arrays.asList(classLoaders); + return this; + } + + public ConfigParams withReader(Reader reader, String suffix) { + SuffixedReader suffixedReader = new SuffixedReader(); + suffixedReader.reader = reader; + suffixedReader.suffix = suffix; + this.reader.add(suffixedReader); + return this; + } + + public ConfigParams withSettings(Settings settings) { + this.settings.add(DatastructureSettings.builder().put(settings.getAsMap()).build()); + return this; + } + + public ConfigParams withDirectoryName(String directoryName) { + this.directoryName = directoryName; + return this; + } + + public ConfigParams withFileNamesWithoutSuffix(String... fileNamesWithoutSuffix) { + this.fileNamesWithoutSuffix.addAll(Arrays.asList(fileNamesWithoutSuffix)); + return this; + } + + public ConfigParams withLocation(String location) { + this.fileLocations.add(location); + return this; + } + + public ConfigParams withPath(String basePath, String basePattern, String path, String pathPattern) throws IOException { + ConfigFinder configFinder = new ConfigFinder(); + configFinder.find(basePath, basePattern, path, pathPattern).getPaths().forEach(this::withLocation); + return this; + } + + public ConfigParams withJdbc(Connection connection, String statement, String[] params) { + JdbcLookup jdbcLookup = new JdbcLookup(); + jdbcLookup.connection = connection; + jdbcLookup.statement = statement; + jdbcLookup.params = params; + jdbcLookups.add(jdbcLookup); + return this; + } + + @Override + public int hashCode() { + return toString().hashCode(); + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + ConfigParams that = (ConfigParams) o; + return withSystemEnvironment == that.withSystemEnvironment && + withSystemProperties == that.withSystemProperties && + failIfEmpty == that.failIfEmpty && + includeAll == that.includeAll && + withStdin == that.withStdin && + withSystemPropertiesOverride == that.withSystemPropertiesOverride && + Objects.equals(classLoaders, that.classLoaders) && + Objects.equals(reader, that.reader) && + Objects.equals(jdbcLookups, that.jdbcLookups) && + Objects.equals(settings, that.settings) && + Objects.equals(args, that.args) && + Objects.equals(directoryName, that.directoryName) && + Objects.equals(fileNamesWithoutSuffix, that.fileNamesWithoutSuffix) && + Objects.equals(fileLocations, that.fileLocations); + } + + @Override + public int compareTo(ConfigParams o) { + return COMPARATOR.compare(this, o); + } + + @Override + public String toString() { + return "" + + withSystemEnvironment + + withSystemProperties + + withStdin + + classLoaders + + reader + + args + + directoryName + + fileNamesWithoutSuffix + + fileLocations; + } + + static class SuffixedReader { + Reader reader; + String suffix; + } + + static class JdbcLookup { + Connection connection; + String statement; + String[] params; + } +} diff --git a/config/src/main/java/org/xbib/config/NullConfigLogger.java b/config/src/main/java/org/xbib/config/NullConfigLogger.java new file mode 100644 index 0000000..fb5458c --- /dev/null +++ b/config/src/main/java/org/xbib/config/NullConfigLogger.java @@ -0,0 +1,19 @@ +package org.xbib.config; + +public class NullConfigLogger implements ConfigLogger { + + public NullConfigLogger() { + } + + @Override + public void info(String string) { + } + + @Override + public void warn(String message) { + } + + @Override + public void error(String message) { + } +} diff --git a/config/src/main/java/org/xbib/config/SystemConfigLogger.java b/config/src/main/java/org/xbib/config/SystemConfigLogger.java new file mode 100644 index 0000000..b85ec46 --- /dev/null +++ b/config/src/main/java/org/xbib/config/SystemConfigLogger.java @@ -0,0 +1,22 @@ +package org.xbib.config; + +public class SystemConfigLogger implements ConfigLogger { + + public SystemConfigLogger() { + } + + @Override + public void info(String string) { + System.err.println("info: " + string); + } + + @Override + public void warn(String message) { + System.err.println("warning: " + message); + } + + @Override + public void error(String message) { + System.err.println("error: " + message); + } +} diff --git a/config/src/main/java/org/xbib/config/package-info.java b/config/src/main/java/org/xbib/config/package-info.java new file mode 100644 index 0000000..675b377 --- /dev/null +++ b/config/src/main/java/org/xbib/config/package-info.java @@ -0,0 +1,4 @@ +/** + * Classes for configuration setup. + */ +package org.xbib.config; diff --git a/config/src/test/java/org/xbib/config/test/ConfigLoaderTest.java b/config/src/test/java/org/xbib/config/test/ConfigLoaderTest.java new file mode 100644 index 0000000..f0e8520 --- /dev/null +++ b/config/src/test/java/org/xbib/config/test/ConfigLoaderTest.java @@ -0,0 +1,76 @@ +package org.xbib.config.test; + +import java.io.IOException; +import java.io.Reader; +import java.io.StringReader; +import org.junit.jupiter.api.Test; +import org.xbib.config.ConfigLoader; +import org.xbib.config.ConfigParams; +import org.xbib.settings.Settings; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; + +public class ConfigLoaderTest { + + @Test + public void configEmptyTest() { + Settings settings = ConfigLoader.getInstance() + .load(new ConfigParams()); + assertTrue(settings.isEmpty()); + } + + @Test + public void configSettingsTest() { + Settings configSettings = Settings.settingsBuilder() + .put("hello", "world") + .build(); + Settings settings = ConfigLoader.getInstance() + .load(new ConfigParams().withSettings(configSettings)); + assertEquals("world", settings.get("hello")); + } + + @Test + public void configArgsTest() { + String[] args = new String[] { + "--config.yaml", + "hello: world" + }; + Settings settings = ConfigLoader.getInstance() + .load(new ConfigParams() + .withArgs(args) + .withFileNamesWithoutSuffix("config")); + assertEquals("world", settings.get("hello")); + } + + @Test + public void configPropertiesTest() { + Reader reader = new StringReader("a=b"); + Settings settings = ConfigLoader.getInstance() + .load(new ConfigParams() + .withReader(reader, "properties")); + assertEquals("b", settings.get("a")); + } + + @Test + public void configFileTest() throws IOException { + Settings settings = ConfigLoader.getInstance() + .load(new ConfigParams() + .withPath(null, null, "src/test/resources", "config.*")); + assertEquals("world", settings.get("hello")); + assertEquals("world2", settings.get("hello2")); + } + + @Test + public void testSystemPropertiesOverride() throws IOException { + System.setProperty("hello", "override"); + Settings settings = ConfigLoader.getInstance() + .load(new ConfigParams() + .withPath(null, null, "src/test/resources", "config.*")); + assertEquals("world", settings.get("hello")); + settings = ConfigLoader.getInstance() + .load(new ConfigParams() + .withSystemPropertiesOverride() + .withPath(null, null, "src/test/resources", "config.*")); + assertEquals("override", settings.get("hello")); + } +} diff --git a/config/src/test/java/org/xbib/config/test/package-info.java b/config/src/test/java/org/xbib/config/test/package-info.java new file mode 100644 index 0000000..b83dd48 --- /dev/null +++ b/config/src/test/java/org/xbib/config/test/package-info.java @@ -0,0 +1,4 @@ +/** + * Test classes for config. + */ +package org.xbib.config.test; diff --git a/config/src/test/resources/META-INF/services/org.xbib.config.ConfigLogger b/config/src/test/resources/META-INF/services/org.xbib.config.ConfigLogger new file mode 100644 index 0000000..3fc2a28 --- /dev/null +++ b/config/src/test/resources/META-INF/services/org.xbib.config.ConfigLogger @@ -0,0 +1,2 @@ +org.xbib.config.NullConfigLogger +org.xbib.config.SystemConfigLogger \ No newline at end of file diff --git a/config/src/test/resources/META-INF/services/org.xbib.settings.SettingsLoader b/config/src/test/resources/META-INF/services/org.xbib.settings.SettingsLoader new file mode 100644 index 0000000..c25e931 --- /dev/null +++ b/config/src/test/resources/META-INF/services/org.xbib.settings.SettingsLoader @@ -0,0 +1,3 @@ +org.xbib.settings.datastructures.PropertiesSettingsLoader +org.xbib.settings.datastructures.json.JsonSettingsLoader +org.xbib.settings.datastructures.yaml.YamlSettingsLoader diff --git a/config/src/test/resources/config.json b/config/src/test/resources/config.json new file mode 100644 index 0000000..300f563 --- /dev/null +++ b/config/src/test/resources/config.json @@ -0,0 +1,3 @@ +{ + "hello2": "world2" +} diff --git a/config/src/test/resources/config.yaml b/config/src/test/resources/config.yaml new file mode 100644 index 0000000..bb56b05 --- /dev/null +++ b/config/src/test/resources/config.yaml @@ -0,0 +1 @@ +hello: world diff --git a/gradle/test/jmh.gradle b/gradle/test/jmh.gradle index 6ae6859..26de618 100644 --- a/gradle/test/jmh.gradle +++ b/gradle/test/jmh.gradle @@ -7,8 +7,8 @@ sourceSets { } dependencies { - jmhImplementation 'org.openjdk.jmh:jmh-core:1.32' - jmhAnnotationProcessor 'org.openjdk.jmh:jmh-generator-annprocess:1.32' + jmhImplementation 'org.openjdk.jmh:jmh-core:1.37' + jmhAnnotationProcessor 'org.openjdk.jmh:jmh-generator-annprocess:1.37' } task jmh(type: JavaExec, group: 'jmh', dependsOn: jmhClasses) { diff --git a/settings-api/build.gradle b/settings-api/build.gradle new file mode 100644 index 0000000..6a1c6eb --- /dev/null +++ b/settings-api/build.gradle @@ -0,0 +1,3 @@ +dependencies { + api project(':datastructures-api') +} diff --git a/settings-api/gradle.properties b/settings-api/gradle.properties new file mode 100644 index 0000000..8d6d281 --- /dev/null +++ b/settings-api/gradle.properties @@ -0,0 +1 @@ +version = 5.0.5 \ No newline at end of file diff --git a/settings-api/src/main/java/module-info.java b/settings-api/src/main/java/module-info.java new file mode 100644 index 0000000..3996b9d --- /dev/null +++ b/settings-api/src/main/java/module-info.java @@ -0,0 +1,10 @@ +import org.xbib.settings.SettingsBuilder; +import org.xbib.settings.SettingsLoader; + +module org.xbib.settings.api { + exports org.xbib.settings; + uses SettingsBuilder; + uses SettingsLoader; + requires transitive org.xbib.datastructures.api; + requires transitive java.sql; +} diff --git a/settings-api/src/main/java/org/xbib/settings/PlaceholderResolver.java b/settings-api/src/main/java/org/xbib/settings/PlaceholderResolver.java new file mode 100644 index 0000000..4523194 --- /dev/null +++ b/settings-api/src/main/java/org/xbib/settings/PlaceholderResolver.java @@ -0,0 +1,16 @@ +package org.xbib.settings; + +/** + * Strategy interface used to resolve replacement values for placeholders contained in Strings. + */ +@FunctionalInterface +public interface PlaceholderResolver { + + /** + * Resolves the supplied placeholder name into the replacement value. + * + * @param placeholderName the name of the placeholder to resolve. + * @return the replacement value or null if no replacement is to be made. + */ + String resolvePlaceholder(String placeholderName); +} diff --git a/settings-api/src/main/java/org/xbib/settings/PropertyPlaceholder.java b/settings-api/src/main/java/org/xbib/settings/PropertyPlaceholder.java new file mode 100644 index 0000000..b308c5f --- /dev/null +++ b/settings-api/src/main/java/org/xbib/settings/PropertyPlaceholder.java @@ -0,0 +1,120 @@ +package org.xbib.settings; + +import java.util.HashSet; +import java.util.Set; + +/** + * + */ +public class PropertyPlaceholder { + + private final String placeholderPrefix; + + private final String placeholderSuffix; + + private final boolean ignoreUnresolvablePlaceholders; + + /** + * Creates a new PropertyPlaceholderHelper that uses the supplied prefix and suffix. + * + * @param placeholderPrefix the prefix that denotes the start of a placeholder. + * @param placeholderSuffix the suffix that denotes the end of a placeholder. + * @param ignoreUnresolvablePlaceholders indicates whether unresolvable placeholders should be ignored + * (true) or cause an exception (false). + */ + public PropertyPlaceholder(String placeholderPrefix, + String placeholderSuffix, + boolean ignoreUnresolvablePlaceholders) { + this.placeholderPrefix = placeholderPrefix; + this.placeholderSuffix = placeholderSuffix; + this.ignoreUnresolvablePlaceholders = ignoreUnresolvablePlaceholders; + } + + /** + * Replaces all placeholders of format ${name} with the value returned from the supplied {@link + * PlaceholderResolver}. + * + * @param value the value containing the placeholders to be replaced. + * @param placeholderResolver the PlaceholderResolver to use for replacement. + * @return the supplied value with placeholders replaced inline. + */ + public String replacePlaceholders(String value, + PlaceholderResolver placeholderResolver) { + return parseStringValue(value, placeholderResolver, new HashSet<>()); + } + + protected String parseStringValue(String value, + PlaceholderResolver placeholderResolver, + Set visitedPlaceholders) { + if (value == null) { + return null; + } + StringBuilder sb = new StringBuilder(value); + int startIndex = value.indexOf(this.placeholderPrefix); + while (startIndex != -1) { + int endIndex = findPlaceholderEndIndex(sb, startIndex); + if (endIndex != -1) { + String placeholder = sb.substring(startIndex + this.placeholderPrefix.length(), endIndex); + if (!visitedPlaceholders.add(placeholder)) { + throw new IllegalArgumentException("Circular placeholder reference '" + placeholder + "' in property definitions"); + } + placeholder = parseStringValue(placeholder, placeholderResolver, visitedPlaceholders); + int defaultValueIdx = placeholder.indexOf(':'); + String defaultValue = null; + if (defaultValueIdx != -1) { + defaultValue = placeholder.substring(defaultValueIdx + 1); + placeholder = placeholder.substring(0, defaultValueIdx); + } + String propVal = placeholderResolver.resolvePlaceholder(placeholder); + if (propVal == null) { + propVal = defaultValue; + } + if (propVal != null) { + propVal = parseStringValue(propVal, placeholderResolver, visitedPlaceholders); + sb.replace(startIndex, endIndex + this.placeholderSuffix.length(), propVal); + startIndex = sb.indexOf(this.placeholderPrefix, startIndex + propVal.length()); + } else if (this.ignoreUnresolvablePlaceholders) { + // Proceed with unprocessed value. + startIndex = sb.indexOf(this.placeholderPrefix, endIndex + this.placeholderSuffix.length()); + } else { + throw new IllegalArgumentException("could not resolve placeholder '" + placeholder + "'"); + } + visitedPlaceholders.remove(placeholder); + } else { + startIndex = -1; + } + } + return sb.toString(); + } + + private int findPlaceholderEndIndex(CharSequence charSequence, int startIndex) { + int index = startIndex + this.placeholderPrefix.length(); + int withinNestedPlaceholder = 0; + while (index < charSequence.length()) { + if (substringMatch(charSequence, index, this.placeholderSuffix)) { + if (withinNestedPlaceholder > 0) { + withinNestedPlaceholder--; + index = index + this.placeholderPrefix.length() - 1; + } else { + return index; + } + } else if (substringMatch(charSequence, index, this.placeholderPrefix)) { + withinNestedPlaceholder++; + index = index + this.placeholderPrefix.length(); + } else { + index++; + } + } + return -1; + } + + private boolean substringMatch(CharSequence charSequence, int index, CharSequence substring) { + for (int j = 0; j < substring.length(); j++) { + int i = index + j; + if (i >= charSequence.length() || charSequence.charAt(i) != substring.charAt(j)) { + return false; + } + } + return true; + } +} diff --git a/settings-api/src/main/java/org/xbib/settings/Settings.java b/settings-api/src/main/java/org/xbib/settings/Settings.java new file mode 100644 index 0000000..142c809 --- /dev/null +++ b/settings-api/src/main/java/org/xbib/settings/Settings.java @@ -0,0 +1,71 @@ +package org.xbib.settings; + +import org.xbib.datastructures.api.ByteSizeValue; +import org.xbib.datastructures.api.TimeValue; +import java.io.IOException; +import java.util.Map; +import java.util.Optional; +import java.util.ServiceLoader; + +public interface Settings extends AutoCloseable { + + class Holder { + + private Holder() { + } + + private static SettingsBuilder createBuilder() { + ServiceLoader serviceLoader = ServiceLoader.load(SettingsBuilder.class); + Optional optionalSettingsBuilder = serviceLoader.findFirst(); + return optionalSettingsBuilder.orElse(null); + } + + private static final Settings emptySettings = createBuilder().build(); + } + + static SettingsBuilder settingsBuilder() { + return Holder.createBuilder(); + } + + static Settings emptySettings() { + return Holder.emptySettings; + } + + boolean isEmpty(); + + String get(String setting); + + String get(String setting, String defaultValue); + + float getAsFloat(String setting, float defaultValue); + + double getAsDouble(String setting, double defaultValue); + + int getAsInt(String setting, int defaultValue); + + long getAsLong(String setting, long defaultValue); + + boolean getAsBoolean(String setting, boolean defaultValue); + + TimeValue getAsTime(String setting, TimeValue defaultValue); + + ByteSizeValue getAsBytesSize(String setting, ByteSizeValue defaultValue); + + String[] getAsArray(String settingPrefix); + + String[] getAsArray(String settingPrefix, String[] defaultArray); + + Map getAsMap(); + + Map getAsStructuredMap(); + + Map getGroups(String prefix); + + Settings getAsSettings(String setting); + + Settings getByPrefix(String prefix); + + boolean containsSetting(String setting); + + void close() throws IOException; +} diff --git a/settings-api/src/main/java/org/xbib/settings/SettingsBuilder.java b/settings-api/src/main/java/org/xbib/settings/SettingsBuilder.java new file mode 100644 index 0000000..1666e37 --- /dev/null +++ b/settings-api/src/main/java/org/xbib/settings/SettingsBuilder.java @@ -0,0 +1,105 @@ +package org.xbib.settings; + +import java.io.IOException; +import java.io.InputStream; +import java.nio.file.Path; +import java.sql.Connection; +import java.sql.PreparedStatement; +import java.sql.ResultSet; +import java.sql.SQLException; +import java.util.List; +import java.util.Map; +import java.util.concurrent.TimeUnit; +import java.util.function.Function; + +public interface SettingsBuilder { + + SettingsBuilder put(String setting, String value); + + SettingsBuilder put(String setting, boolean value); + + SettingsBuilder put(String setting, int value); + + SettingsBuilder put(String setting, long value); + + SettingsBuilder put(String setting, float value); + + SettingsBuilder put(String setting, double value); + + SettingsBuilder putArray(String setting, String... values); + + SettingsBuilder putArray(String setting, List values); + + SettingsBuilder put(String settingPrefix, String groupName, String[] settings, String[] values) + throws SettingsException; + + SettingsBuilder put(Settings settings); + + SettingsBuilder put(Map settings); + + SettingsBuilder loadFromString(String resourceName, String content); + + SettingsBuilder loadFromResource(String resourceName, InputStream inputStream); + + default SettingsBuilder fromJdbc(Connection connection, String statement, String[] params) throws SQLException { + try (PreparedStatement preparedStatement = connection.prepareStatement(statement, params); + ResultSet resultSet = preparedStatement.executeQuery()) { + while (resultSet.next()) { + String key = resultSet.getString("key"); + String value = resultSet.getString("value"); + put(key, value); + } + } + return this; + } + + SettingsBuilder loadFromSystemProperties(); + + SettingsBuilder loadFromSystemEnvironment(); + + /** + * Runs across all the settings set on this builder and replaces {@code ${...}} elements in the + * each setting value according to the following logic: + * First, tries to resolve it against a System property ({@link System#getProperty(String)}), next, + * tries and resolve it against an environment variable ({@link System#getenv(String)}), next, + * tries and resolve it against a date pattern to resolve the current date, + * and last, tries and replace it with another setting already set on this builder. + * @param propertyPlaceholder the property place holder + * @param placeholderResolver the place holder resolver + * @return this builder + */ + SettingsBuilder replacePropertyPlaceholders(PropertyPlaceholder propertyPlaceholder, + PlaceholderResolver placeholderResolver); + + /** + * A default method to replace property placeholders. + * @return this builder + */ + SettingsBuilder replacePropertyPlaceholders(); + + /** + * Optional settings refresh mechanism, using reloading from a path after a give time period. + * May not be implemented at all. + */ + SettingsBuilder setRefresh(Path path, long initialDelay, long period, TimeUnit timeUnit); + + /** + * Map all settings keys and values to other keys and values. + * Example usage is to override settings from another priority source. + * @return this builder + */ + SettingsBuilder map(Function, Map.Entry> function); + + /** + * Return the Settings from this SettingsBuilder. + * @return the settings + */ + Settings build(); + + /** + * Returns true if the settings builder is empty. + * @return true if empty + */ + boolean isEmpty(); + +} diff --git a/settings-api/src/main/java/org/xbib/settings/SettingsException.java b/settings-api/src/main/java/org/xbib/settings/SettingsException.java new file mode 100644 index 0000000..77f46cb --- /dev/null +++ b/settings-api/src/main/java/org/xbib/settings/SettingsException.java @@ -0,0 +1,16 @@ +package org.xbib.settings; + +/** + * A generic failure to handle settings. + */ +@SuppressWarnings("serial") +public class SettingsException extends RuntimeException { + + public SettingsException(String message) { + super(message); + } + + public SettingsException(String message, Throwable cause) { + super(message, cause); + } +} diff --git a/settings-api/src/main/java/org/xbib/settings/SettingsLoader.java b/settings-api/src/main/java/org/xbib/settings/SettingsLoader.java new file mode 100644 index 0000000..69cbb09 --- /dev/null +++ b/settings-api/src/main/java/org/xbib/settings/SettingsLoader.java @@ -0,0 +1,33 @@ +package org.xbib.settings; + +import java.io.IOException; +import java.util.Map; +import java.util.Set; + +/** + * Provides the ability to load settings from + * the actual source content that represents them. + */ +public interface SettingsLoader { + + /** + * Suffices for file names to load from. + * @return a set of suffices + */ + Set suffixes(); + + /** + * Loads the settings from a source string. + * @param source the source + * @return a Map + */ + Map load(String source) throws IOException; + + /** + * Loads the settings from a map. + * @param source the map with the source + * @return a Map + */ + Map load(Map source) throws IOException; + +} diff --git a/settings-api/src/main/java/org/xbib/settings/SettingsLoaderService.java b/settings-api/src/main/java/org/xbib/settings/SettingsLoaderService.java new file mode 100644 index 0000000..54a2d02 --- /dev/null +++ b/settings-api/src/main/java/org/xbib/settings/SettingsLoaderService.java @@ -0,0 +1,54 @@ +package org.xbib.settings; + +import java.util.HashMap; +import java.util.HashSet; +import java.util.Map; +import java.util.ServiceLoader; +import java.util.Set; + +/** + * A settings loader service for loading {@link SettingsLoader} implementations. + */ +public final class SettingsLoaderService { + + private static final SettingsLoaderService INSTANCE = new SettingsLoaderService(); + + private final Map, SettingsLoader> settingsLoaderMap; + + private SettingsLoaderService() { + this.settingsLoaderMap = new HashMap<>(); + ServiceLoader serviceLoader = ServiceLoader.load(SettingsLoader.class); + for (SettingsLoader settingsLoader : serviceLoader) { + settingsLoaderMap.put(settingsLoader.suffixes(), settingsLoader); + } + } + + public static SettingsLoaderService getInstance() { + return INSTANCE; + } + + /** + * Returns a {@link SettingsLoader} based on the resource name. + * @param resourceName the resource + * @return the settings loader + */ + public SettingsLoader loaderFromResource(String resourceName) { + for (Map.Entry, SettingsLoader> entry : settingsLoaderMap.entrySet()) { + Set suffixes = entry.getKey(); + for (String suffix : suffixes) { + if (resourceName.endsWith(suffix)) { + return entry.getValue(); + } + } + } + throw new IllegalArgumentException("no settings loader for " + resourceName + " in " + settingsLoaderMap.keySet()); + } + + public Set getSuffixes() { + Set suffixes = new HashSet<>(); + for (Set set : settingsLoaderMap.keySet()) { + suffixes.addAll(set); + } + return suffixes; + } +} diff --git a/settings-api/src/main/java/org/xbib/settings/package-info.java b/settings-api/src/main/java/org/xbib/settings/package-info.java new file mode 100644 index 0000000..ecce19c --- /dev/null +++ b/settings-api/src/main/java/org/xbib/settings/package-info.java @@ -0,0 +1,4 @@ +/** + * Settings API. + */ +package org.xbib.settings; diff --git a/settings-datastructures-json/build.gradle b/settings-datastructures-json/build.gradle new file mode 100644 index 0000000..b8922b0 --- /dev/null +++ b/settings-datastructures-json/build.gradle @@ -0,0 +1,4 @@ +dependencies { + api project(':settings-datastructures') + api project(':datastructures-json-tiny') +} diff --git a/settings-datastructures-json/gradle.properties b/settings-datastructures-json/gradle.properties new file mode 100644 index 0000000..8d6d281 --- /dev/null +++ b/settings-datastructures-json/gradle.properties @@ -0,0 +1 @@ +version = 5.0.5 \ No newline at end of file diff --git a/settings-datastructures-json/src/main/java/module-info.java b/settings-datastructures-json/src/main/java/module-info.java new file mode 100644 index 0000000..418127f --- /dev/null +++ b/settings-datastructures-json/src/main/java/module-info.java @@ -0,0 +1,10 @@ +import org.xbib.settings.SettingsLoader; +import org.xbib.settings.datastructures.json.JsonSettingsLoader; + +module org.xbib.settings.datastructures.json { + exports org.xbib.settings.datastructures.json; + requires transitive org.xbib.settings.datastructures; + requires org.xbib.datastructures.json.tiny; + uses SettingsLoader; + provides SettingsLoader with JsonSettingsLoader; +} diff --git a/settings-datastructures-json/src/main/java/org/xbib/settings/datastructures/json/JsonSettingsLoader.java b/settings-datastructures-json/src/main/java/org/xbib/settings/datastructures/json/JsonSettingsLoader.java new file mode 100644 index 0000000..b023e99 --- /dev/null +++ b/settings-datastructures-json/src/main/java/org/xbib/settings/datastructures/json/JsonSettingsLoader.java @@ -0,0 +1,22 @@ +package org.xbib.settings.datastructures.json; + +import org.xbib.settings.datastructures.AbstractSettingsLoader; +import org.xbib.datastructures.api.DataStructure; +import org.xbib.datastructures.json.tiny.Json; +import java.util.Set; + +public class JsonSettingsLoader extends AbstractSettingsLoader { + + public JsonSettingsLoader() { + } + + @Override + public DataStructure dataStructure() { + return new Json(); + } + + @Override + public Set suffixes() { + return Set.of("json"); + } +} diff --git a/settings-datastructures-json/src/main/java/org/xbib/settings/datastructures/json/package-info.java b/settings-datastructures-json/src/main/java/org/xbib/settings/datastructures/json/package-info.java new file mode 100644 index 0000000..615dd6e --- /dev/null +++ b/settings-datastructures-json/src/main/java/org/xbib/settings/datastructures/json/package-info.java @@ -0,0 +1,4 @@ +/** + * JSON settings with the datastructures package. + */ +package org.xbib.settings.datastructures.json; diff --git a/settings-datastructures-json/src/main/resources/META-INF.services/org.xbib.settings.SettingsLoader b/settings-datastructures-json/src/main/resources/META-INF.services/org.xbib.settings.SettingsLoader new file mode 100644 index 0000000..7d341dc --- /dev/null +++ b/settings-datastructures-json/src/main/resources/META-INF.services/org.xbib.settings.SettingsLoader @@ -0,0 +1 @@ +org.xbib.settings.datastructures.json.JsonSettingsLoader diff --git a/settings-datastructures-json/src/test/java/org/xbib/settings/datastructures/json/test/JsonSettingsTest.java b/settings-datastructures-json/src/test/java/org/xbib/settings/datastructures/json/test/JsonSettingsTest.java new file mode 100644 index 0000000..2622986 --- /dev/null +++ b/settings-datastructures-json/src/test/java/org/xbib/settings/datastructures/json/test/JsonSettingsTest.java @@ -0,0 +1,82 @@ +package org.xbib.settings.datastructures.json.test; + +import org.junit.jupiter.api.Test; +import org.xbib.settings.Settings; +import org.xbib.settings.SettingsLoader; +import org.xbib.settings.datastructures.json.JsonSettingsLoader; +import java.io.IOException; +import java.util.Arrays; +import java.util.HashMap; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +public class JsonSettingsTest { + + @Test + public void testMapForSettings() throws IOException { + Map map = new HashMap<>(); + map.put("hello", "world"); + Map settingsMap = new HashMap<>(); + settingsMap.put("map", map); + SettingsLoader settingsLoader = new JsonSettingsLoader(); + Settings settings = Settings.settingsBuilder() + .put(settingsLoader.load(settingsMap)) + .build(); + assertEquals("{map.hello=world}", settings.getAsMap().toString()); + } + + @Test + public void testMapSettingsFromReader() throws IOException { + Map map = Map.of("map", Map.of("hello", "world")); + SettingsLoader settingsLoader = new JsonSettingsLoader(); + Settings settings = Settings.settingsBuilder() + .put(settingsLoader.load(map)) + .build(); + assertEquals("{map.hello=world}", settings.getAsMap().toString()); + } + + @Test + public void testLoadFromString() throws IOException { + String json = "{\"Hello\":\"World\"}"; + SettingsLoader loader = new JsonSettingsLoader(); + Map result = loader.load(json); + assertEquals("{Hello=World}", result.toString()); + } + + @Test + public void testLoadSettingsFromString() { + String json = "{\"Hello\":\"World\"}"; + Settings settings = Settings.settingsBuilder().loadFromString("json", json).build(); + assertEquals("{Hello=World}", settings.getAsMap().toString()); + } + + @Test + public void testFlatLoader() throws IOException { + String s = "{\"a\":{\"b\":\"c\"}}"; + SettingsLoader loader = new JsonSettingsLoader(); + Map flatMap = loader.load(s); + assertEquals("{a.b=c}", flatMap.toString()); + } + + @Test + public void testLoadFromMap() throws IOException { + Map map = new LinkedHashMap<>(); + Map code = new LinkedHashMap<>(); + code.put("a", "b"); + code.put("b", "c"); + Map name = new LinkedHashMap<>(); + name.put("a", "b"); + name.put("b", "c"); + List list = Arrays.asList("a", "b"); + map.put("code", code); + map.put("name", name); + map.put("list", list); + map.put("null", null); + SettingsLoader loader = new JsonSettingsLoader(); + Map result = loader.load(map); + assertEquals("{code.a=b, code.b=c, name.a=b, name.b=c, list.0=a, list.1=b, null=null}", result.toString()); + } +} diff --git a/settings-datastructures-json/src/test/java/org/xbib/settings/datastructures/json/test/package-info.java b/settings-datastructures-json/src/test/java/org/xbib/settings/datastructures/json/test/package-info.java new file mode 100644 index 0000000..0ffad2a --- /dev/null +++ b/settings-datastructures-json/src/test/java/org/xbib/settings/datastructures/json/test/package-info.java @@ -0,0 +1,4 @@ +/** + * Testing JSON settings with the datastructures package. + */ +package org.xbib.settings.datastructures.json.test; diff --git a/settings-datastructures-json/src/test/resources/META-INF/services/org.xbib.settings.SettingsLoader b/settings-datastructures-json/src/test/resources/META-INF/services/org.xbib.settings.SettingsLoader new file mode 100644 index 0000000..7d341dc --- /dev/null +++ b/settings-datastructures-json/src/test/resources/META-INF/services/org.xbib.settings.SettingsLoader @@ -0,0 +1 @@ +org.xbib.settings.datastructures.json.JsonSettingsLoader diff --git a/settings-datastructures-json/src/test/resources/org/xbib/settings/datastructures/json/test/test-settings.json b/settings-datastructures-json/src/test/resources/org/xbib/settings/datastructures/json/test/test-settings.json new file mode 100644 index 0000000..f949d38 --- /dev/null +++ b/settings-datastructures-json/src/test/resources/org/xbib/settings/datastructures/json/test/test-settings.json @@ -0,0 +1,3 @@ +{ + "a": "b" +} diff --git a/settings-datastructures-yaml/build.gradle b/settings-datastructures-yaml/build.gradle new file mode 100644 index 0000000..d22cec5 --- /dev/null +++ b/settings-datastructures-yaml/build.gradle @@ -0,0 +1,4 @@ +dependencies { + api project(':settings-datastructures') + api project(':datastructures-yaml-tiny') +} diff --git a/settings-datastructures-yaml/gradle.properties b/settings-datastructures-yaml/gradle.properties new file mode 100644 index 0000000..8d6d281 --- /dev/null +++ b/settings-datastructures-yaml/gradle.properties @@ -0,0 +1 @@ +version = 5.0.5 \ No newline at end of file diff --git a/settings-datastructures-yaml/src/main/java/module-info.java b/settings-datastructures-yaml/src/main/java/module-info.java new file mode 100644 index 0000000..5310e4c --- /dev/null +++ b/settings-datastructures-yaml/src/main/java/module-info.java @@ -0,0 +1,10 @@ +import org.xbib.settings.SettingsLoader; +import org.xbib.settings.datastructures.yaml.YamlSettingsLoader; + +module org.xbib.settings.datastructures.yaml { + exports org.xbib.settings.datastructures.yaml; + requires transitive org.xbib.settings.datastructures; + requires org.xbib.datastructures.yaml.tiny; + uses SettingsLoader; + provides SettingsLoader with YamlSettingsLoader; +} diff --git a/settings-datastructures-yaml/src/main/java/org/xbib/settings/datastructures/yaml/YamlSettingsLoader.java b/settings-datastructures-yaml/src/main/java/org/xbib/settings/datastructures/yaml/YamlSettingsLoader.java new file mode 100644 index 0000000..11647a7 --- /dev/null +++ b/settings-datastructures-yaml/src/main/java/org/xbib/settings/datastructures/yaml/YamlSettingsLoader.java @@ -0,0 +1,31 @@ +package org.xbib.settings.datastructures.yaml; + +import org.xbib.settings.datastructures.AbstractSettingsLoader; +import org.xbib.datastructures.api.DataStructure; +import org.xbib.datastructures.yaml.tiny.Yaml; + +import java.io.IOException; +import java.util.Map; +import java.util.Set; + +public class YamlSettingsLoader extends AbstractSettingsLoader { + + public YamlSettingsLoader() { + } + + @Override + public DataStructure dataStructure() { + return new Yaml(); + } + + @Override + public Set suffixes() { + return Set.of("yml", "yaml"); + } + + @Override + public Map load(String source) throws IOException { + // replace tabs with whitespace (yaml does not accept tabs, but many users might use it still...) + return super.load(source.replace("\t", " ")); + } +} diff --git a/settings-datastructures-yaml/src/main/java/org/xbib/settings/datastructures/yaml/package-info.java b/settings-datastructures-yaml/src/main/java/org/xbib/settings/datastructures/yaml/package-info.java new file mode 100644 index 0000000..78f0f18 --- /dev/null +++ b/settings-datastructures-yaml/src/main/java/org/xbib/settings/datastructures/yaml/package-info.java @@ -0,0 +1,4 @@ +/** + * YAML settings with the datastructures package. + */ +package org.xbib.settings.datastructures.yaml; diff --git a/settings-datastructures-yaml/src/main/resources/META-INF/services/org.xbib.content.settings.datastructures.SettingsLoader b/settings-datastructures-yaml/src/main/resources/META-INF/services/org.xbib.content.settings.datastructures.SettingsLoader new file mode 100644 index 0000000..3ab4266 --- /dev/null +++ b/settings-datastructures-yaml/src/main/resources/META-INF/services/org.xbib.content.settings.datastructures.SettingsLoader @@ -0,0 +1 @@ +org.xbib.settings.datastructures.yaml.YamlSettingsLoader diff --git a/settings-datastructures-yaml/src/test/java/org/xbib/settings/datastructures/yaml/test/YamlSettingsTest.java b/settings-datastructures-yaml/src/test/java/org/xbib/settings/datastructures/yaml/test/YamlSettingsTest.java new file mode 100644 index 0000000..d154c43 --- /dev/null +++ b/settings-datastructures-yaml/src/test/java/org/xbib/settings/datastructures/yaml/test/YamlSettingsTest.java @@ -0,0 +1,67 @@ +package org.xbib.settings.datastructures.yaml.test; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +import org.junit.jupiter.api.Test; +import org.xbib.settings.Settings; +import org.xbib.settings.SettingsLoader; +import org.xbib.settings.datastructures.yaml.YamlSettingsLoader; +import java.io.IOException; +import java.util.Arrays; +import java.util.HashMap; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; + +public class YamlSettingsTest { + + @Test + public void testMapForSettings() throws IOException { + Map map = new HashMap<>(); + map.put("hello", "world"); + Map settingsMap = new HashMap<>(); + settingsMap.put("map", map); + SettingsLoader settingsLoader = new YamlSettingsLoader(); + Settings settings = Settings.settingsBuilder() + .put(settingsLoader.load(settingsMap)) + .build(); + assertEquals("{map.hello=world}", settings.getAsMap().toString()); + } + + @Test + public void testMapSettingsFromReader() throws IOException { + Map map = Map.of("map", Map.of("hello", "world")); + SettingsLoader settingsLoader = new YamlSettingsLoader(); + Settings settings = Settings.settingsBuilder() + .put(settingsLoader.load(map)) + .build(); + assertEquals("{map.hello=world}", settings.getAsMap().toString()); + } + + @Test + public void testFlatLoader() throws IOException { + String s = "a:\n b: c\n"; + SettingsLoader loader = new YamlSettingsLoader(); + Map flatMap = loader.load(s); + assertEquals("{a.b=c}", flatMap.toString()); + } + + @Test + public void testLoadFromMap() throws IOException { + Map map = new LinkedHashMap<>(); + Map code = new LinkedHashMap<>(); + code.put("a", "b"); + code.put("b", "c"); + Map name = new LinkedHashMap<>(); + name.put("a", "b"); + name.put("b", "c"); + List list = Arrays.asList("a", "b"); + map.put("code", code); + map.put("name", name); + map.put("list", list); + map.put("null", null); + SettingsLoader loader = new YamlSettingsLoader(); + Map result = loader.load(map); + assertEquals("{code.a=b, code.b=c, name.a=b, name.b=c, list.0=a, list.1=b, null=null}", result.toString()); + } +} diff --git a/settings-datastructures-yaml/src/test/java/org/xbib/settings/datastructures/yaml/test/package-info.java b/settings-datastructures-yaml/src/test/java/org/xbib/settings/datastructures/yaml/test/package-info.java new file mode 100644 index 0000000..9233c9e --- /dev/null +++ b/settings-datastructures-yaml/src/test/java/org/xbib/settings/datastructures/yaml/test/package-info.java @@ -0,0 +1,4 @@ +/** + * Testing YAML settings with the datastructures package. + */ +package org.xbib.settings.datastructures.yaml.test; diff --git a/settings-datastructures/build.gradle b/settings-datastructures/build.gradle new file mode 100644 index 0000000..971e01b --- /dev/null +++ b/settings-datastructures/build.gradle @@ -0,0 +1,4 @@ +dependencies { + api project(':settings-api') + api project(':datastructures-tiny') +} diff --git a/settings-datastructures/gradle.properties b/settings-datastructures/gradle.properties new file mode 100644 index 0000000..8d6d281 --- /dev/null +++ b/settings-datastructures/gradle.properties @@ -0,0 +1 @@ +version = 5.0.5 \ No newline at end of file diff --git a/settings-datastructures/src/main/java/module-info.java b/settings-datastructures/src/main/java/module-info.java new file mode 100644 index 0000000..6bacf68 --- /dev/null +++ b/settings-datastructures/src/main/java/module-info.java @@ -0,0 +1,15 @@ +import org.xbib.settings.SettingsBuilder; +import org.xbib.settings.SettingsLoader; +import org.xbib.settings.datastructures.DatastructureSettingsBuilder; +import org.xbib.settings.datastructures.PropertiesSettingsLoader; + +module org.xbib.settings.datastructures { + uses SettingsLoader; + provides SettingsLoader with PropertiesSettingsLoader; + uses SettingsBuilder; + provides SettingsBuilder with DatastructureSettingsBuilder; + exports org.xbib.settings.datastructures; + requires transitive org.xbib.settings.api; + requires org.xbib.datastructures.tiny; + requires transitive org.xbib.datastructures.api; +} diff --git a/settings-datastructures/src/main/java/org/xbib/settings/datastructures/AbstractSettingsLoader.java b/settings-datastructures/src/main/java/org/xbib/settings/datastructures/AbstractSettingsLoader.java new file mode 100644 index 0000000..96b182a --- /dev/null +++ b/settings-datastructures/src/main/java/org/xbib/settings/datastructures/AbstractSettingsLoader.java @@ -0,0 +1,80 @@ +package org.xbib.settings.datastructures; + +import org.xbib.settings.SettingsLoader; +import org.xbib.datastructures.api.Builder; +import org.xbib.datastructures.api.DataStructure; +import org.xbib.datastructures.api.ListNode; +import org.xbib.datastructures.api.MapNode; +import org.xbib.datastructures.api.Node; +import org.xbib.datastructures.api.Parser; +import org.xbib.datastructures.api.ValueNode; +import org.xbib.datastructures.tiny.TinyMap; + +import java.io.IOException; +import java.io.Reader; +import java.io.StringReader; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; + +public abstract class AbstractSettingsLoader implements SettingsLoader { + + public AbstractSettingsLoader() { + } + + public abstract DataStructure dataStructure(); + + @Override + public Map load(Map map) throws IOException { + Builder builder = dataStructure().createBuilder(); + builder.buildMap(map); + return load(builder.build()); + } + + @Override + public Map load(String source) throws IOException { + Parser parser = dataStructure().createParser(); + return load(parser, new StringReader(source)); + } + + private Map load(Parser parser, Reader reader) throws IOException { + List path = new ArrayList<>(); + TinyMap.Builder map = TinyMap.builder(); + Node node = parser.parse(reader); + parseObject(node, map, path, null); + return map.build(); + } + + private void parseObject(Node node, + TinyMap.Builder map, + List path, + CharSequence name) { + if (node instanceof ValueNode) { + ValueNode valueNode = (ValueNode) node; + StringBuilder sb = new StringBuilder(); + for (CharSequence s : path) { + sb.append(s).append('.'); + } + sb.append(name); + Object object = valueNode.get(); + map.put(sb.toString(), object != null ? object.toString() : null); + } else if (node instanceof ListNode) { + ListNode listNode = (ListNode) node; + int counter = 0; + for (Node nn : listNode.get()) { + parseObject(nn, map, path, name + "." + (counter++)); + } + } else if (node instanceof MapNode) { + if (name != null) { + path.add(name); + } + MapNode mapNode = (MapNode) node; + for (Map.Entry> me : mapNode.get().entrySet()) { + parseObject(me.getValue(), map, path, me.getKey()); + } + if (name != null) { + path.remove(path.size() - 1); + } + } + } +} diff --git a/settings-datastructures/src/main/java/org/xbib/settings/datastructures/DatastructureSettings.java b/settings-datastructures/src/main/java/org/xbib/settings/datastructures/DatastructureSettings.java new file mode 100644 index 0000000..6234521 --- /dev/null +++ b/settings-datastructures/src/main/java/org/xbib/settings/datastructures/DatastructureSettings.java @@ -0,0 +1,367 @@ +package org.xbib.settings.datastructures; + +import org.xbib.settings.Settings; +import org.xbib.settings.SettingsException; +import org.xbib.datastructures.api.ByteSizeValue; +import org.xbib.datastructures.api.TimeValue; +import org.xbib.datastructures.tiny.TinyMap; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; + +public class DatastructureSettings implements Settings { + + private static final String[] EMPTY_ARRAY = new String[0]; + + private final TinyMap map; + + DatastructureSettings(TinyMap map) { + this.map = map; + } + + public static DatastructureSettingsBuilder builder() { + return new DatastructureSettingsBuilder(); + } + + public static DatastructureSettings fromMap(Map map) { + DatastructureSettingsBuilder builder = new DatastructureSettingsBuilder(); + for (Map.Entry entry : map.entrySet()) { + builder.put(entry.getKey(), entry.getValue() != null ? entry.getValue().toString() : null); + } + return builder.build(); + } + + public static void toMap(DatastructureSettings settings, Map map) { + for (String key : settings.getAsMap().keySet()) { + map.put(key, settings.get(key)); + } + } + + public static String[] splitStringByCommaToArray(String s) { + return splitStringToArray(s, ','); + } + + public static String[] splitStringToArray(String s, char c) { + if (s.length() == 0) { + return EMPTY_ARRAY; + } + final char[] chars = s.toCharArray(); + int count = 1; + for (final char x : chars) { + if (x == c) { + count++; + } + } + final String[] result = new String[count]; + final int len = chars.length; + int start = 0; + int pos = 0; + int i = 0; + for (; pos < len; pos++) { + if (chars[pos] == c) { + int size = pos - start; + if (size > 0) { + result[i++] = new String(chars, start, size); + } + start = pos + 1; + } + } + int size = pos - start; + if (size > 0) { + result[i++] = new String(chars, start, size); + } + if (i != count) { + String[] result1 = new String[i]; + System.arraycopy(result, 0, result1, 0, i); + return result1; + } + return result; + } + + @Override + public Map getAsMap() { + return this.map; + } + + @Override + public Map getAsStructuredMap() { + TinyMap.Builder stringObjectMap = TinyMap.builder(); + for (Map.Entry entry : map.entrySet()) { + String key = entry.getKey(); + String value = entry.getValue(); + processSetting(stringObjectMap, "", key, value); + } + for (Map.Entry entry : stringObjectMap.entrySet()) { + String key = entry.getKey(); + Object object = entry.getValue(); + if (object instanceof Map) { + @SuppressWarnings("unchecked") + Map valMap = (Map) object; + stringObjectMap.put(key, convertMapsToArrays(valMap)); + } + } + return stringObjectMap.build(); + } + + @Override + public Settings getByPrefix(String prefix) { + DatastructureSettingsBuilder builder = new DatastructureSettingsBuilder(); + for (Map.Entry entry : map.entrySet()) { + String key = entry.getKey(); + String value = entry.getValue(); + if (key.startsWith(prefix)) { + if (key.length() < prefix.length()) { + continue; + } + builder.put(key.substring(prefix.length()), value); + } + } + return builder.build(); + } + + @Override + public Settings getAsSettings(String setting) { + return getByPrefix(setting + "."); + } + + @Override + public boolean containsSetting(String setting) { + if (map.containsKey(setting)) { + return true; + } + for (String key : map.keySet()) { + if (key.startsWith(setting)) { + return true; + } + } + return false; + } + + @Override + public String get(String setting) { + return map.get(setting); + } + + @Override + public String get(String setting, String defaultValue) { + String s = map.get(setting); + return s == null ? defaultValue : s; + } + + @Override + public float getAsFloat(String setting, float defaultValue) { + String s = get(setting); + try { + return s == null ? defaultValue : Float.parseFloat(s); + } catch (NumberFormatException e) { + throw new SettingsException("Failed to parse float setting [" + setting + "] with value [" + s + "]", e); + } + } + + @Override + public double getAsDouble(String setting, double defaultValue) { + String s = get(setting); + try { + return s == null ? defaultValue : Double.parseDouble(s); + } catch (NumberFormatException e) { + throw new SettingsException("Failed to parse double setting [" + setting + "] with value [" + s + "]", e); + } + } + + @Override + public int getAsInt(String setting, int defaultValue) { + String s = get(setting); + try { + return s == null ? defaultValue : Integer.parseInt(s); + } catch (NumberFormatException e) { + throw new SettingsException("Failed to parse int setting [" + setting + "] with value [" + s + "]", e); + } + } + + @Override + public long getAsLong(String setting, long defaultValue) { + String s = get(setting); + try { + return s == null ? defaultValue : Long.parseLong(s); + } catch (NumberFormatException e) { + throw new SettingsException("Failed to parse long setting [" + setting + "] with value [" + s + "]", e); + } + } + + @Override + public boolean getAsBoolean(String setting, boolean defaultValue) { + String value = get(setting); + if (value == null) { + return defaultValue; + } + return !("false".equals(value) || "0".equals(value) || "off".equals(value) || "no".equals(value)); + } + + @Override + public TimeValue getAsTime(String setting, TimeValue defaultValue) { + return TimeValue.parseTimeValue(get(setting), defaultValue); + } + + @Override + public ByteSizeValue getAsBytesSize(String setting, ByteSizeValue defaultValue) { + return ByteSizeValue.parseBytesSizeValue(get(setting), defaultValue); + } + + @Override + public String[] getAsArray(String settingPrefix) { + return getAsArray(settingPrefix, EMPTY_ARRAY); + } + + @Override + public String[] getAsArray(String settingPrefix, String[] defaultArray) { + List result = new ArrayList<>(); + if (get(settingPrefix) != null) { + String[] strings = splitStringByCommaToArray(get(settingPrefix)); + if (strings.length > 0) { + for (String string : strings) { + result.add(string.trim()); + } + } + } + int counter = 0; + while (true) { + String value = get(settingPrefix + '.' + (counter++)); + if (value == null) { + break; + } + result.add(value.trim()); + } + if (result.isEmpty()) { + return defaultArray; + } + return result.toArray(new String[0]); + } + + @Override + public Map getGroups(String prefix) { + String settingPrefix = prefix; + if (settingPrefix.charAt(settingPrefix.length() - 1) != '.') { + settingPrefix = settingPrefix + "."; + } + // we don't really care that it might happen twice + TinyMap.Builder> hashMap = TinyMap.builder(); + for (String o : this.map.keySet()) { + if (o.startsWith(settingPrefix)) { + String nameValue = o.substring(settingPrefix.length()); + int dotIndex = nameValue.indexOf('.'); + if (dotIndex == -1) { + throw new SettingsException("failed to get setting group for [" + + settingPrefix + + "] setting prefix and setting [" + o + "] because of a missing '.'"); + } + String name = nameValue.substring(0, dotIndex); + String value = nameValue.substring(dotIndex + 1); + Map groupSettings = hashMap.computeIfAbsent(name, k -> TinyMap.builder()); + groupSettings.put(value, get(o)); + } + } + TinyMap.Builder retVal = TinyMap.builder(); + for (Map.Entry> entry : hashMap.entrySet()) { + String key = entry.getKey(); + TinyMap.Builder value = entry.getValue(); + retVal.put(key, new DatastructureSettings(value.build())); + } + return retVal.build(); + } + + @Override + public boolean equals(Object o) { + return this == o || !(o == null || getClass() != o.getClass()) && map.equals(((DatastructureSettings) o).map); + } + + @Override + public int hashCode() { + return map.hashCode(); + } + + private void processSetting(Map map, String prefix, String setting, String value) { + int prefixLength = setting.indexOf('.'); + if (prefixLength == -1) { + @SuppressWarnings("unchecked") + Map innerMap = (Map) map.get(prefix + setting); + if (innerMap != null) { + for (Map.Entry e : innerMap.entrySet()) { + String k = e.getKey(); + Object v = e.getValue(); + map.put(prefix + setting + "." + k, v); + } + } + map.put(prefix + setting, value); + } else { + String key = setting.substring(0, prefixLength); + String rest = setting.substring(prefixLength + 1); + Object existingValue = map.get(prefix + key); + if (existingValue == null) { + Map newMap = TinyMap.builder(); + processSetting(newMap, "", rest, value); + map.put(key, newMap); + } else { + if (existingValue instanceof Map) { + @SuppressWarnings("unchecked") + Map innerMap = (Map) existingValue; + processSetting(innerMap, "", rest, value); + map.put(key, innerMap); + } else { + processSetting(map, prefix + key + ".", rest, value); + } + } + } + } + + private Object convertMapsToArrays(Map map) { + if (map.isEmpty()) { + return map; + } + boolean isArray = true; + int maxIndex = -1; + for (Map.Entry entry : map.entrySet()) { + String key = entry.getKey(); + Object value = entry.getValue(); + if (isArray) { + try { + int index = Integer.parseInt(key); + if (index >= 0) { + maxIndex = Math.max(maxIndex, index); + } else { + isArray = false; + } + } catch (NumberFormatException ex) { + isArray = false; + } + } + if (value instanceof Map) { + @SuppressWarnings("unchecked") + Map valMap = (Map) value; + map.put(key, convertMapsToArrays(valMap)); + } + } + if (isArray && (maxIndex + 1) == map.size()) { + ArrayList newValue = new ArrayList<>(maxIndex + 1); + for (int i = 0; i <= maxIndex; i++) { + Object obj = map.get(Integer.toString(i)); + if (obj == null) { + return map; + } + newValue.add(obj); + } + return newValue; + } + return map; + } + + public boolean isEmpty() { + return map.isEmpty(); + } + + @Override + public void close() throws IOException { + // do nothing + } +} diff --git a/settings-datastructures/src/main/java/org/xbib/settings/datastructures/DatastructureSettingsBuilder.java b/settings-datastructures/src/main/java/org/xbib/settings/datastructures/DatastructureSettingsBuilder.java new file mode 100644 index 0000000..6179506 --- /dev/null +++ b/settings-datastructures/src/main/java/org/xbib/settings/datastructures/DatastructureSettingsBuilder.java @@ -0,0 +1,332 @@ +package org.xbib.settings.datastructures; + +import org.xbib.settings.PlaceholderResolver; +import org.xbib.settings.PropertyPlaceholder; +import org.xbib.settings.Settings; +import org.xbib.settings.SettingsBuilder; +import org.xbib.settings.SettingsException; +import org.xbib.settings.SettingsLoader; +import org.xbib.settings.SettingsLoaderService; +import org.xbib.datastructures.tiny.TinyMap; + +import java.io.BufferedReader; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.nio.charset.StandardCharsets; +import java.nio.file.Path; +import java.time.DateTimeException; +import java.time.LocalDate; +import java.time.format.DateTimeFormatter; +import java.util.List; +import java.util.Map; +import java.util.concurrent.TimeUnit; +import java.util.function.Function; +import java.util.stream.Collectors; + +/** + * + */ +public class DatastructureSettingsBuilder implements SettingsBuilder { + + private final SettingsLoaderService settingsLoaderService; + + private final TinyMap.Builder map; + + public DatastructureSettingsBuilder() { + this.settingsLoaderService = SettingsLoaderService.getInstance(); + this.map = TinyMap.builder(); + } + + public String remove(String key) { + return map.remove(key); + } + + @Override + public boolean isEmpty() { + return map.isEmpty(); + } + + /** + * Sets a setting with the provided setting key and value. + * + * @param key The setting key + * @param value The setting value + * @return The builder + */ + @Override + public DatastructureSettingsBuilder put(String key, String value) { + map.put(key, value); + return this; + } + + /** + * Sets the setting with the provided setting key and the boolean value. + * + * @param setting The setting key + * @param value The boolean value + * @return The builder + */ + @Override + public DatastructureSettingsBuilder put(String setting, boolean value) { + put(setting, String.valueOf(value)); + return this; + } + + /** + * Sets the setting with the provided setting key and the int value. + * + * @param setting The setting key + * @param value The int value + * @return The builder + */ + @Override + public DatastructureSettingsBuilder put(String setting, int value) { + put(setting, String.valueOf(value)); + return this; + } + + /** + * Sets the setting with the provided setting key and the long value. + * + * @param setting The setting key + * @param value The long value + * @return The builder + */ + @Override + public DatastructureSettingsBuilder put(String setting, long value) { + put(setting, String.valueOf(value)); + return this; + } + + /** + * Sets the setting with the provided setting key and the float value. + * + * @param setting The setting key + * @param value The float value + * @return The builder + */ + @Override + public DatastructureSettingsBuilder put(String setting, float value) { + put(setting, String.valueOf(value)); + return this; + } + + /** + * Sets the setting with the provided setting key and the double value. + * + * @param setting The setting key + * @param value The double value + * @return The builder + */ + @Override + public DatastructureSettingsBuilder put(String setting, double value) { + put(setting, String.valueOf(value)); + return this; + } + + /** + * Sets the setting with the provided setting key and an array of values. + * + * @param setting The setting key + * @param values The values + * @return The builder + */ + @Override + public DatastructureSettingsBuilder putArray(String setting, String... values) { + remove(setting); + int counter = 0; + while (true) { + String value = map.remove(setting + '.' + (counter++)); + if (value == null) { + break; + } + } + for (int i = 0; i < values.length; i++) { + put(setting + '.' + i, values[i]); + } + return this; + } + + /** + * Sets the setting with the provided setting key and an array of values. + * + * @param setting The setting key + * @param values The values + * @return The builder + */ + @Override + public DatastructureSettingsBuilder putArray(String setting, List values) { + remove(setting); + int counter = 0; + while (true) { + String value = map.remove(setting + '.' + (counter++)); + if (value == null) { + break; + } + } + for (int i = 0; i < values.size(); i++) { + put(setting + '.' + i, values.get(i)); + } + return this; + } + + /** + * Sets the setting group. + * + * @param settingPrefix setting prefix + * @param groupName group name + * @param settings settings + * @param values values + * @return a builder + * @throws SettingsException if setting fails + */ + @Override + public DatastructureSettingsBuilder put(String settingPrefix, String groupName, String[] settings, String[] values) + throws SettingsException { + if (settings.length != values.length) { + throw new SettingsException("the settings length must match the value length"); + } + for (int i = 0; i < settings.length; i++) { + if (values[i] == null) { + continue; + } + put(settingPrefix + "" + groupName + "." + settings[i], values[i]); + } + return this; + } + + /** + * Sets all the provided settings. + * + * @param settings settings + * @return builder + */ + @Override + public DatastructureSettingsBuilder put(Settings settings) { + map.putAll(settings.getAsMap()); + return this; + } + + /** + * Sets all the provided settings. + * + * @param settings settings + * @return a builder + */ + @Override + public DatastructureSettingsBuilder put(Map settings) { + map.putAll(settings); + return this; + } + + /** + * Loads settings from a resource. + * + * @param resourceName resource name + * @param inputStream input stream + * @return builder + */ + @Override + public DatastructureSettingsBuilder loadFromResource(String resourceName, InputStream inputStream) { + SettingsLoader settingsLoader = settingsLoaderService.loaderFromResource(resourceName); + try (BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(inputStream, StandardCharsets.UTF_8))) { + Map loadedSettings = settingsLoader.load(bufferedReader.lines().collect(Collectors.joining())); + put(loadedSettings); + } catch (Exception e) { + throw new SettingsException("failed to load settings from [" + resourceName + "]", e); + } + return this; + } + + /** + * Loads settings from the actual string content that represents them using the + * {@link SettingsLoaderService#loaderFromResource(String)} (String)}. + * + * @param resourceName the resource name + * @param source the source + * @return builder + */ + @Override + public DatastructureSettingsBuilder loadFromString(String resourceName, String source) { + SettingsLoader settingsLoader = settingsLoaderService.loaderFromResource(resourceName); + try { + put(settingsLoader.load(source)); + } catch (Exception e) { + throw new SettingsException("failed to load settings from [" + source + "]", e); + } + return this; + } + + /** + * Load system properties to this settings. + * + * @return builder + */ + @Override + public DatastructureSettingsBuilder loadFromSystemProperties() { + for (Map.Entry entry : System.getProperties().entrySet()) { + put((String) entry.getKey(), (String) entry.getValue()); + } + return this; + } + + /** + * Load system environment to this settings. + * + * @return builder + */ + @Override + public DatastructureSettingsBuilder loadFromSystemEnvironment() { + for (Map.Entry entry : System.getenv().entrySet()) { + put(entry.getKey(), entry.getValue()); + } + return this; + } + + @Override + public DatastructureSettingsBuilder replacePropertyPlaceholders(PropertyPlaceholder propertyPlaceholder, + PlaceholderResolver placeholderResolver) { + map.replaceAll((k, v) -> propertyPlaceholder.replacePlaceholders(v, placeholderResolver)); + return this; + } + + @Override + public DatastructureSettingsBuilder replacePropertyPlaceholders() { + return replacePropertyPlaceholders(new PropertyPlaceholder("${", "}", false), + placeholderName -> { + // system property + String value = System.getProperty(placeholderName); + if (value != null) { + return value; + } + // environment + value = System.getenv(placeholderName); + if (value != null) { + return value; + } + // current date + try { + return DateTimeFormatter.ofPattern(placeholderName).format(LocalDate.now()); + } catch (IllegalArgumentException | DateTimeException e) { + return map.get(placeholderName); + } + } + ); + } + + @Override + public DatastructureSettingsBuilder setRefresh(Path path, long initialDelay, long period, TimeUnit timeUnit) { + return this; + } + + @Override + public SettingsBuilder map(Function, Map.Entry> function) { + map.entrySet().stream().map(function).forEach(e -> put(e.getKey(), e.getValue())); + return this; + } + + @Override + public DatastructureSettings build() { + return new DatastructureSettings(map.build()); + } +} diff --git a/settings-datastructures/src/main/java/org/xbib/settings/datastructures/PropertiesSettingsLoader.java b/settings-datastructures/src/main/java/org/xbib/settings/datastructures/PropertiesSettingsLoader.java new file mode 100644 index 0000000..7e21857 --- /dev/null +++ b/settings-datastructures/src/main/java/org/xbib/settings/datastructures/PropertiesSettingsLoader.java @@ -0,0 +1,47 @@ +package org.xbib.settings.datastructures; + +import org.xbib.settings.SettingsLoader; +import org.xbib.datastructures.tiny.TinyMap; +import java.io.IOException; +import java.io.StringReader; +import java.util.Map; +import java.util.Properties; +import java.util.Set; + +/** + * Settings loader that loads (parses) the settings in a properties format. + */ +public class PropertiesSettingsLoader implements SettingsLoader { + + public PropertiesSettingsLoader() { + } + + @Override + public Set suffixes() { + return Set.of("properties"); + } + + @Override + public Map load(String source) throws IOException { + Properties props = new Properties(); + try (StringReader reader = new StringReader(source)) { + props.load(reader); + TinyMap.Builder result = TinyMap.builder(); + for (Map.Entry entry : props.entrySet()) { + result.put((String) entry.getKey(), (String) entry.getValue()); + } + return result.build(); + } + } + + @Override + public Map load(Map source) { + Properties props = new Properties(); + props.putAll(source); + TinyMap.Builder result = TinyMap.builder(); + for (Map.Entry entry : props.entrySet()) { + result.put((String) entry.getKey(), (String) entry.getValue()); + } + return result.build(); + } +} diff --git a/settings-datastructures/src/main/java/org/xbib/settings/datastructures/package-info.java b/settings-datastructures/src/main/java/org/xbib/settings/datastructures/package-info.java new file mode 100644 index 0000000..52b8711 --- /dev/null +++ b/settings-datastructures/src/main/java/org/xbib/settings/datastructures/package-info.java @@ -0,0 +1,4 @@ +/** + * Classes for settings using the datastructures API. + */ +package org.xbib.settings.datastructures; diff --git a/settings-datastructures/src/main/resources/META-INF/services/org.xbib.settings.SettingsBuilder b/settings-datastructures/src/main/resources/META-INF/services/org.xbib.settings.SettingsBuilder new file mode 100644 index 0000000..872ade2 --- /dev/null +++ b/settings-datastructures/src/main/resources/META-INF/services/org.xbib.settings.SettingsBuilder @@ -0,0 +1 @@ +org.xbib.settings.datastructures.DatastructureSettingsBuilder \ No newline at end of file diff --git a/settings-datastructures/src/test/java/org/xbib/settings/datastructures/test/SettingsTest.java b/settings-datastructures/src/test/java/org/xbib/settings/datastructures/test/SettingsTest.java new file mode 100644 index 0000000..52005f5 --- /dev/null +++ b/settings-datastructures/src/test/java/org/xbib/settings/datastructures/test/SettingsTest.java @@ -0,0 +1,134 @@ +package org.xbib.settings.datastructures.test; + +import org.junit.jupiter.api.Test; +import org.xbib.settings.Settings; + +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.time.LocalDate; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.Map; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertTrue; + +public class SettingsTest { + + @Test + public void testEmpty() { + Settings settings = Settings.emptySettings(); + assertTrue(settings.isEmpty()); + } + + @Test + public void testSimpleSettings() { + Settings settings = Settings.settingsBuilder() + .put("a", "b") + .build(); + assertEquals("{a=b}", settings.getAsMap().toString()); + assertEquals("{a=b}", settings.getAsStructuredMap().toString()); + } + + @Test + public void testArray() { + Settings settings = Settings.settingsBuilder() + .putArray("input", Arrays.asList("a", "b", "c")).build(); + assertEquals("a", settings.getAsArray("input")[0]); + assertEquals("b", settings.getAsArray("input")[1]); + assertEquals("c", settings.getAsArray("input")[2]); + } + + @SuppressWarnings("unchecked") + @Test + public void testArrayOfMaps() { + Settings settings = Settings.settingsBuilder() + .put("location.0.code", "Code 0") + .put("location.0.name", "Name 0") + .put("location.1.code", "Code 1") + .put("location.1.name", "Name 1") + .build(); + + // turn map with index keys 0,1,... into a list of maps + Map map = settings.getAsSettings("location").getAsStructuredMap(); + List> list = new ArrayList<>(); + for (Map.Entry entry : map.entrySet()) { + list.add((Map) entry.getValue()); + } + assertEquals("[{code=Code 0, name=Name 0}, {code=Code 1, name=Name 1}]", list.toString()); + } + + @Test + public void testGroups() { + Settings settings = Settings.settingsBuilder() + .put("prefix.group1.k1", "v1") + .put("prefix.group1.k2", "v2") + .put("prefix.group1.k3", "v3") + .put("prefix.group2.k1", "v1") + .put("prefix.group2.k2", "v2") + .put("prefix.group2.k3", "v3") + .build(); + Map groups = settings.getGroups("prefix"); + assertEquals("[group1, group2]", groups.keySet().toString()); + assertTrue(groups.get("group1").getAsMap().containsKey("k1")); + assertTrue(groups.get("group1").getAsMap().containsKey("k2")); + assertTrue(groups.get("group1").getAsMap().containsKey("k3")); + assertTrue(groups.get("group2").getAsMap().containsKey("k1")); + assertTrue(groups.get("group2").getAsMap().containsKey("k2")); + assertTrue(groups.get("group2").getAsMap().containsKey("k3")); + } + + @Test + public void testCurrentYearInSettings() { + Settings settings = Settings.settingsBuilder() + .put("date", "${yyyy}") + .replacePropertyPlaceholders() + .build(); + assertEquals(LocalDate.now().getYear(), Integer.parseInt(settings.get("date"))); + } + + @Test + public void testPropertyReplaceNull() { + Settings settings = Settings.settingsBuilder() + .put("null", null) + .replacePropertyPlaceholders() + .build(); + assertNull(settings.get("null")); + } + + @Test + public void testSystemEnvironment() { + Settings settings = Settings.settingsBuilder() + .loadFromSystemEnvironment() + .build(); + assertFalse(settings.getAsMap().isEmpty()); + } + + @Test + public void testSystemProperties() { + Settings settings = Settings.settingsBuilder() + .loadFromSystemProperties() + .build(); + assertFalse(settings.getAsMap().isEmpty()); + } + + @Test + public void testPropertiesLoaderFromResource() { + Settings settings = Settings.settingsBuilder() + .loadFromResource("properties", new ByteArrayInputStream("a.b=c".getBytes(StandardCharsets.UTF_8))) + .build(); + assertEquals("{a.b=c}", settings.getAsMap().toString()); + } + + @Test + public void testPropertiesLoaderFromString() { + Settings settings = Settings.settingsBuilder() + .loadFromString("properties", "#\na.b=c") + .build(); + assertEquals("{a.b=c}", settings.getAsMap().toString()); + } +} diff --git a/settings-datastructures/src/test/resources/META-INF/services/org.xbib.settings.SettingsLoader b/settings-datastructures/src/test/resources/META-INF/services/org.xbib.settings.SettingsLoader new file mode 100644 index 0000000..d3ebb87 --- /dev/null +++ b/settings-datastructures/src/test/resources/META-INF/services/org.xbib.settings.SettingsLoader @@ -0,0 +1 @@ +org.xbib.settings.datastructures.PropertiesSettingsLoader diff --git a/settings.gradle b/settings.gradle index f95db6f..96c9cf2 100644 --- a/settings.gradle +++ b/settings.gradle @@ -65,3 +65,8 @@ include 'datastructures-yaml-tiny' include 'datastructures-validation' include 'datastructures-trie' include 'datastructures-raml' +include 'settings-api' +include 'settings-datastructures' +include 'settings-datastructures-json' +include 'settings-datastructures-yaml' +include 'config'