diff --git a/content-config/build.gradle b/content-config/build.gradle index 918bc9b..d3cea2e 100644 --- a/content-config/build.gradle +++ b/content-config/build.gradle @@ -1,4 +1,5 @@ dependencies { - api project(':content-json') - api project(':content-yaml') + api project(':content-settings-datastructures') + testImplementation project(':content-settings-datastructures-json') + testImplementation project(':content-settings-datastructures-yaml') } diff --git a/content-config/src/main/java/module-info.java b/content-config/src/main/java/module-info.java index 670dfa8..daad3bc 100644 --- a/content-config/src/main/java/module-info.java +++ b/content-config/src/main/java/module-info.java @@ -1,5 +1,10 @@ +import org.xbib.content.config.ConfigLogger; +import org.xbib.content.settings.datastructures.SettingsLoader; + module org.xbib.content.config { exports org.xbib.content.config; - requires org.xbib.content.json; - requires org.xbib.content.yaml; + uses ConfigLogger; + uses SettingsLoader; + provides ConfigLogger with org.xbib.content.config.SystemConfigLogger; + requires transitive org.xbib.content.settings.datastructures; } diff --git a/content-config/src/main/java/org/xbib/content/config/ConfigException.java b/content-config/src/main/java/org/xbib/content/config/ConfigException.java new file mode 100644 index 0000000..a5a887e --- /dev/null +++ b/content-config/src/main/java/org/xbib/content/config/ConfigException.java @@ -0,0 +1,17 @@ +package org.xbib.content.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/content-config/src/main/java/org/xbib/content/config/ConfigFinder.java b/content-config/src/main/java/org/xbib/content/config/ConfigFinder.java new file mode 100644 index 0000000..b4c5655 --- /dev/null +++ b/content-config/src/main/java/org/xbib/content/config/ConfigFinder.java @@ -0,0 +1,167 @@ +package org.xbib.content.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/content-config/src/main/java/org/xbib/content/config/ConfigLoader.java b/content-config/src/main/java/org/xbib/content/config/ConfigLoader.java index b783d4f..bcd13bd 100644 --- a/content-config/src/main/java/org/xbib/content/config/ConfigLoader.java +++ b/content-config/src/main/java/org/xbib/content/config/ConfigLoader.java @@ -1,9 +1,9 @@ package org.xbib.content.config; -import org.xbib.content.json.JsonSettingsLoader; -import org.xbib.content.settings.Settings; -import org.xbib.content.SettingsLoader; -import org.xbib.content.yaml.YamlSettingsLoader; +import org.xbib.content.settings.datastructures.SettingsLoader; +import org.xbib.content.settings.datastructures.Settings; +import org.xbib.content.settings.datastructures.SettingsLoaderService; + import java.io.BufferedReader; import java.io.IOException; import java.io.InputStream; @@ -14,157 +14,257 @@ import java.nio.charset.StandardCharsets; import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.Paths; -import java.util.Arrays; +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; /** - * A configuration file loader for JSON/YAML configuration files. + * A configuration loader for configuration files. */ public class ConfigLoader { - private static final String JSON = ".json"; + private final Map map; - private static final String YML = ".yml"; + private ConfigLogger logger; - private static final String YAML = ".yaml"; + private ConfigLoader() { + this.map = new HashMap<>(); + } - private final ConfigLogger logger; + private static class Holder { + private static ConfigLogger createConfigLogger() { + ServiceLoader serviceLoader = ServiceLoader.load(ConfigLogger.class); + Optional optionalConfigLogger = serviceLoader.findFirst(); + return optionalConfigLogger.orElse(new SystemConfigLogger()); + } + static ConfigLoader LOADER = new ConfigLoader().withLogger(createConfigLogger()); + } - public ConfigLoader(ConfigLogger logger) { + public static ConfigLoader getInstance() { + return Holder.LOADER; + } + + public ConfigLoader withLogger(ConfigLogger logger) { this.logger = logger; + return this; } - public Settings.Builder loadSettings(String[] args, - ClassLoader classLoader, - String applicationName, - String... fileNamesWithoutSuffix) throws IOException { - Settings.Builder settings = createSettingsFromArgs(args, applicationName, fileNamesWithoutSuffix); - return settings != null ? settings : loadSettings(classLoader, applicationName, fileNamesWithoutSuffix); + public synchronized Settings load(ConfigParams configParams) throws ConfigException { + map.computeIfAbsent(configParams, p -> internalLoad(p).build()); + return map.get(configParams); } - public Settings.Builder loadSettings(ClassLoader classLoader, - String applicationName, - String... fileNamesWithoutSuffix) throws IOException { - Settings.Builder settings = createSettingsFromStdin(); - if (settings != null) { - return overrideFromProperties(applicationName, settings); + private Settings.Builder internalLoad(ConfigParams params) throws ConfigException { + Settings.Builder settings = Settings.settingsBuilder(); + if (params.withSystemEnvironment) { + settings.loadFromSystemEnvironment(); } - for (String fileNameWithoutSuffix : fileNamesWithoutSuffix) { - settings = createSettingsFromFile(createListOfLocations(applicationName, fileNameWithoutSuffix)); - if (settings != null) { - return overrideFromProperties(applicationName, settings); + if (params.withSystemProperties) { + settings.loadFromSystemProperties(); + } + if (!params.settings.isEmpty()) { + for (Settings s : params.settings) { + settings.put(s); } - for (ClassLoader cl : List.of(classLoader, - Thread.currentThread().getContextClassLoader(), - ConfigLoader.class.getClassLoader(), - ClassLoader.getSystemClassLoader())) { - if (cl != null) { - settings = createClasspathSettings(cl, applicationName, fileNameWithoutSuffix); - if (settings != null) { - return overrideFromProperties(applicationName, settings); + } + if (!params.reader.isEmpty()) { + for (ConfigParams.SuffixedReader reader : params.reader) { + Settings.Builder readerSettings = createSettingsFromReader(reader.reader, reader.suffix); + if (readerSettings != null) { + settings.put(readerSettings.build()); + if (!params.includeAll) { + return settings; } } } } - throw new IllegalArgumentException("no config found for " + applicationName + " " + - Arrays.asList(fileNamesWithoutSuffix)); - } - - private Settings.Builder createSettingsFromArgs(String[] args, - String applicationName, - String... fileNamesWithoutSuffix) throws IOException { - for (String fileNameWithoutSuffix : fileNamesWithoutSuffix) { - for (String suffix : List.of(YML, YAML, JSON)) { - for (int i = 0; i < args.length - 1; i++) { - String arg = args[i]; - if (arg.equals("--" + applicationName + "-" + fileNameWithoutSuffix + suffix)) { - return createSettingsFromReader(new StringReader(args[i + 1]), suffix); - } - } - } - } - return null; - } - - private Settings.Builder createSettingsFromStdin() throws IOException { - if (System.in != null) { - int numBytesWaiting = System.in.available(); - if (numBytesWaiting > 0) { - String suffix = System.getProperty("config.format", "yaml"); - return createSettingsFromStream(System.in, "." + suffix); - } - } - return null; - } - - private Settings.Builder createSettingsFromFile(List settingsFileNames) throws IOException { - for (String settingsFileName: settingsFileNames) { - int pos = settingsFileName.lastIndexOf('.'); - String suffix = (pos > 0 ? settingsFileName.substring(pos) : "").toLowerCase(Locale.ROOT); - Path path = Paths.get(settingsFileName); - logger.info("trying " + path.toString()); - if (Files.exists(path)) { - logger.info("found path: " + path); - System.setProperty("config.path", path.getParent().toString()); - return createSettingsFromStream(Files.newInputStream(path), suffix); - } - } - return null; - } - - private Settings.Builder createClasspathSettings(ClassLoader classLoader, - String applicationName, - String fileNameWithoutSuffix) - throws IOException { - for (String suffix : List.of(YML, YAML, JSON)) { - InputStream inputStream = classLoader.getResourceAsStream(applicationName + '-' + - fileNameWithoutSuffix + suffix); - if (inputStream != null) { - logger.info("found resource: " + applicationName + '-' + fileNameWithoutSuffix + suffix); - Settings.Builder settings = createSettingsFromStream(inputStream, suffix); - if (settings != null) { + if (params.args != null) { + Settings.Builder argsSettings = createSettingsFromArgs(params); + if (argsSettings != null) { + settings.put(argsSettings.build()); + if (!params.includeAll) { return settings; } } } + if (params.withStdin) { + Settings.Builder stdinSettings = createSettingsFromStdin(); + if (stdinSettings != null) { + settings.put(stdinSettings.build()); + if (!params.includeAll) { + return overrideFromProperties(params, settings); + } + } + } + if (!params.fileLocations.isEmpty()) { + Settings.Builder 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) { + Settings.Builder 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) { + Settings.Builder classpathSettings = createClasspathSettings(params, cl, fileNameWithoutSuffix); + if (classpathSettings != null) { + settings.put(classpathSettings.build()); + if (!params.includeAll) { + return overrideFromProperties(params, settings); + } + } + } + } + } + } + } + if (params.includeAll) { + return overrideFromProperties(params, settings); + } + throw new ConfigException("no config found"); + } + + private Settings.Builder 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 Settings.Builder createSettingsFromStream(InputStream inputStream, String suffix) throws IOException { + private Settings.Builder 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 Settings.Builder createSettingsFromFile(List settingsFileNames) throws ConfigException { + Settings.Builder 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); + Settings.Builder fileSettings = createSettingsFromStream(inputStream, suffix); + if (fileSettings != null) { + settings.put(fileSettings.build()); + } + } catch (Exception e) { + throw new ConfigException(e); + } + } + } + return settings.isEmpty() ? null : settings; + } + + private Settings.Builder createClasspathSettings(ConfigParams params, + ClassLoader classLoader, + String fileNameWithoutSuffix) throws ConfigException { + Settings.Builder 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); + } + Settings.Builder streamSettings = createSettingsFromStream(inputStream, suffix); + if (streamSettings != null) { + settings.put(streamSettings.build()); + } + } + } + return settings.isEmpty() ? null : settings; + } + + private Settings.Builder createSettingsFromStream(InputStream inputStream, + String suffix) throws ConfigException { if (inputStream == null) { - logger.error("unable to open input stream"); + if (logger != null) { + logger.error("unable to open input stream"); + } return null; } return createSettingsFromReader(new InputStreamReader(inputStream, StandardCharsets.UTF_8), suffix); } - private Settings.Builder createSettingsFromReader(Reader reader, String suffix) throws IOException { + private Settings.Builder createSettingsFromReader(Reader reader, + String suffix) throws ConfigException { if (reader == null) { - logger.error("unable to open reader"); + if (logger != null) { + logger.error("unable to open reader"); + } return null; } - SettingsLoader settingsLoader = isYaml(suffix) ? new YamlSettingsLoader() : - isJson(suffix) ? new JsonSettingsLoader() : null; + SettingsLoader settingsLoader = SettingsLoaderService.getInstance().loaderFromResource(suffix); if (settingsLoader != null) { Settings.Builder 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 { - logger.error("suffix is invalid: " + suffix); + if (logger != null) { + logger.error("suffix is invalid: " + suffix); + } } return null; } - private static Settings.Builder overrideFromProperties(String applicationName, Settings.Builder settings) { - for (Map.Entry entry : settings.map().entrySet()) { - String key = entry.getKey(); - String value = System.getProperty(applicationName + '.' + key); + private Settings.Builder overrideFromProperties(ConfigParams params, + Settings.Builder settings) { + for (String key : settings.map().keySet()) { + String value = System.getProperty(params.directoryName != null ? params.directoryName + '.' + key : key); if (value != null) { settings.put(key, value); } @@ -172,28 +272,24 @@ public class ConfigLoader { return settings; } - private static List createListOfLocations(String applicationName, String fileNameWithoutSuffix) { - String xdgConfigHome = System.getenv("XDG_CONFIG_HOME"); - if (xdgConfigHome == null) { - xdgConfigHome = System.getProperty("user.home") + "/.config"; + 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.of( - applicationName + '-' + fileNameWithoutSuffix + YML, - applicationName + '-' + fileNameWithoutSuffix + YAML, - applicationName + '-' + fileNameWithoutSuffix + JSON, - xdgConfigHome + '/' + applicationName + '/' + fileNameWithoutSuffix + YML, - xdgConfigHome + '/' + applicationName + '/' + fileNameWithoutSuffix + YAML, - xdgConfigHome + '/' + applicationName + '/' + fileNameWithoutSuffix + JSON, - "/etc/" + applicationName + '/' + fileNameWithoutSuffix + YML, - "/etc/" + applicationName + '/' + fileNameWithoutSuffix + YAML, - "/etc/" + applicationName + '/' + fileNameWithoutSuffix + JSON); - } - - private static boolean isYaml(String suffix) { - return YAML.equals(suffix) || YML.equals(suffix); - } - - private static boolean isJson(String suffix) { - return JSON.equals(suffix); + return list; } } diff --git a/content-config/src/main/java/org/xbib/content/config/ConfigParams.java b/content-config/src/main/java/org/xbib/content/config/ConfigParams.java new file mode 100644 index 0000000..a23b749 --- /dev/null +++ b/content-config/src/main/java/org/xbib/content/config/ConfigParams.java @@ -0,0 +1,129 @@ +package org.xbib.content.config; + +import org.xbib.content.settings.datastructures.Settings; + +import java.io.IOException; +import java.io.Reader; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Comparator; +import java.util.List; + +public class ConfigParams implements Comparable { + + private static final Comparator COMPARATOR = + Comparator.comparing(ConfigParams::toString); + + boolean withSystemEnvironment = false; + + boolean withSystemProperties = false; + + boolean includeAll = false; + + boolean withStdin = false; + + List classLoaders = null; + + final List reader = 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 includeAll() { + this.includeAll = 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(settings); + 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; + } + + @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; + } + + public static class SuffixedReader { + Reader reader; + String suffix; + } +} diff --git a/content-config/src/main/resources/META-INF/services/org.xbib.content.config.ConfigLogger b/content-config/src/main/resources/META-INF/services/org.xbib.content.config.ConfigLogger new file mode 100644 index 0000000..1c7eb79 --- /dev/null +++ b/content-config/src/main/resources/META-INF/services/org.xbib.content.config.ConfigLogger @@ -0,0 +1 @@ +org.xbib.content.config.SystemConfigLogger \ No newline at end of file diff --git a/content-config/src/test/java/org/xbib/content/config/test/ConfigLoaderTest.java b/content-config/src/test/java/org/xbib/content/config/test/ConfigLoaderTest.java new file mode 100644 index 0000000..da83e0d --- /dev/null +++ b/content-config/src/test/java/org/xbib/content/config/test/ConfigLoaderTest.java @@ -0,0 +1,45 @@ +package org.xbib.content.config.test; + +import org.junit.jupiter.api.Test; +import org.xbib.content.config.ConfigLoader; +import org.xbib.content.config.ConfigParams; +import org.xbib.content.settings.datastructures.Settings; + +import java.io.IOException; +import java.io.Reader; +import java.io.StringReader; +import java.util.logging.Level; +import java.util.logging.Logger; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +public class ConfigLoaderTest { + + @Test + public void configTest() { + 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 configInterlibraryTest() { + Settings settings = ConfigLoader.getInstance() + .load(new ConfigParams() + .withDirectoryName("interlibrary") + .withFileNamesWithoutSuffix("test")); + Logger.getAnonymousLogger().log(Level.INFO, settings.getAsMap().toString()); + } +} diff --git a/content-config/src/test/resources/META-INF/services/org.xbib.content.settings.datastructures.SettingsLoader b/content-config/src/test/resources/META-INF/services/org.xbib.content.settings.datastructures.SettingsLoader new file mode 100644 index 0000000..76ef2b0 --- /dev/null +++ b/content-config/src/test/resources/META-INF/services/org.xbib.content.settings.datastructures.SettingsLoader @@ -0,0 +1,3 @@ +org.xbib.content.settings.datastructures.PropertiesSettingsLoader +org.xbib.content.settings.datastructures.json.JsonSettingsLoader +org.xbib.content.settings.datastructures.yaml.YamlSettingsLoader diff --git a/content-config/src/test/resources/config.json b/content-config/src/test/resources/config.json new file mode 100644 index 0000000..300f563 --- /dev/null +++ b/content-config/src/test/resources/config.json @@ -0,0 +1,3 @@ +{ + "hello2": "world2" +} diff --git a/content-config/src/test/resources/config.yaml b/content-config/src/test/resources/config.yaml new file mode 100644 index 0000000..bb56b05 --- /dev/null +++ b/content-config/src/test/resources/config.yaml @@ -0,0 +1 @@ +hello: world diff --git a/content-core/build.gradle b/content-core/build.gradle index ffcf648..ca4258e 100644 --- a/content-core/build.gradle +++ b/content-core/build.gradle @@ -1,5 +1,4 @@ dependencies { api project(':content-api') - api "org.xbib:datastructures-tiny:${project.property('xbib-datastructures-tiny.version')}" api "com.fasterxml.jackson.core:jackson-core:${project.property('jackson.version')}" } diff --git a/content-core/src/main/java/module-info.java b/content-core/src/main/java/module-info.java index ce44129..c6172b4 100644 --- a/content-core/src/main/java/module-info.java +++ b/content-core/src/main/java/module-info.java @@ -11,7 +11,6 @@ module org.xbib.content.core { exports org.xbib.content.util.unit; exports org.xbib.content.core; requires transitive org.xbib.content; - requires transitive org.xbib.datastructures.tiny; requires com.fasterxml.jackson.core; provides SettingsLoader with PropertiesSettingsLoader; } diff --git a/content-core/src/main/java/org/xbib/content/core/AbstractXContentParser.java b/content-core/src/main/java/org/xbib/content/core/AbstractXContentParser.java index 69bbab7..1efe8ca 100644 --- a/content-core/src/main/java/org/xbib/content/core/AbstractXContentParser.java +++ b/content-core/src/main/java/org/xbib/content/core/AbstractXContentParser.java @@ -1,10 +1,8 @@ package org.xbib.content.core; import org.xbib.content.XContentParser; -import org.xbib.datastructures.tiny.TinyMap; import java.io.IOException; import java.util.ArrayList; -import java.util.HashMap; import java.util.List; import java.util.Map; @@ -13,83 +11,17 @@ import java.util.Map; */ public abstract class AbstractXContentParser implements XContentParser { - private static final MapFactory SIMPLE_MAP_FACTORY = HashMap::new; + //private static final MapFactory SIMPLE_MAP_FACTORY = HashMap::new; - private static final MapFactory TINY_MAP_FACTORY = TinyMap::builder; + //private static final MapFactory TINY_MAP_FACTORY = TinyMap::builder; private boolean losslessDecimals; private boolean base16Checks; - private static Map readMap(XContentParser parser) throws IOException { - return readMap(parser, SIMPLE_MAP_FACTORY); - } + protected abstract MapFactory getMapFactory(); - private static Map readOrderedMap(XContentParser parser) throws IOException { - return readMap(parser, TINY_MAP_FACTORY); - } - - private static Map readMap(XContentParser parser, MapFactory mapFactory) throws IOException { - Map map = mapFactory.newMap(); - XContentParser.Token t = parser.currentToken(); - if (t == null) { - t = parser.nextToken(); - } - if (t == XContentParser.Token.START_OBJECT) { - t = parser.nextToken(); - } - for (; t == XContentParser.Token.FIELD_NAME; t = parser.nextToken()) { - String fieldName = parser.currentName(); - t = parser.nextToken(); - Object value = readValue(parser, mapFactory, t); - map.put(fieldName, value); - } - return map; - } - - private static List readList(XContentParser parser, MapFactory mapFactory) throws IOException { - ArrayList list = new ArrayList<>(); - Token t; - while ((t = parser.nextToken()) != XContentParser.Token.END_ARRAY) { - list.add(readValue(parser, mapFactory, t)); - } - return list; - } - - private static Object readValue(XContentParser parser, MapFactory mapFactory, XContentParser.Token t) throws IOException { - if (t == XContentParser.Token.VALUE_NULL) { - return null; - } else if (t == XContentParser.Token.VALUE_STRING) { - if (parser.isBase16Checks()) { - return XContentHelper.parseBase16(parser.text()); - } - return parser.text(); - } else if (t == XContentParser.Token.VALUE_NUMBER) { - XContentParser.NumberType numberType = parser.numberType(); - if (numberType == XContentParser.NumberType.INT) { - return parser.isLosslessDecimals() ? parser.bigIntegerValue() : parser.intValue(); - } else if (numberType == XContentParser.NumberType.LONG) { - return parser.isLosslessDecimals() ? parser.bigIntegerValue() : parser.longValue(); - } else if (numberType == XContentParser.NumberType.FLOAT) { - return parser.isLosslessDecimals() ? parser.bigDecimalValue() : parser.floatValue(); - } else if (numberType == XContentParser.NumberType.DOUBLE) { - return parser.isLosslessDecimals() ? parser.bigDecimalValue() : parser.doubleValue(); - } else if (numberType == NumberType.BIG_INTEGER) { - return parser.bigIntegerValue(); - } else if (numberType == NumberType.BIG_DECIMAL) { - return parser.bigDecimalValue(); - } - } else if (t == XContentParser.Token.VALUE_BOOLEAN) { - return parser.booleanValue(); - } else if (t == XContentParser.Token.START_OBJECT) { - return readMap(parser, mapFactory); - } else if (t == XContentParser.Token.START_ARRAY) { - return readList(parser, mapFactory); - } else if (t == XContentParser.Token.VALUE_EMBEDDED_OBJECT) { - return parser.binaryValue(); - } - return null; - } + protected abstract MapFactory getOrderedMapFactory(); @Override public boolean isBooleanValue() throws IOException { @@ -233,11 +165,6 @@ public abstract class AbstractXContentParser implements XContentParser { } } - @FunctionalInterface - interface MapFactory { - Map newMap(); - } - /** * Returns true if the a sequence of chars is one of "true","false","on","off","yes","no","0","1". * @@ -266,4 +193,74 @@ public abstract class AbstractXContentParser implements XContentParser { return length == 5 && (text[offset] == 'f' && text[offset + 1] == 'a' && text[offset + 2] == 'l' && text[offset + 3] == 's' && text[offset + 4] == 'e'); } + + private Map readMap(XContentParser parser) throws IOException { + return readMap(parser, getMapFactory()); + } + + private Map readOrderedMap(XContentParser parser) throws IOException { + return readMap(parser, getOrderedMapFactory()); + } + + private static Map readMap(XContentParser parser, MapFactory mapFactory) throws IOException { + Map map = mapFactory.newMap(); + XContentParser.Token t = parser.currentToken(); + if (t == null) { + t = parser.nextToken(); + } + if (t == XContentParser.Token.START_OBJECT) { + t = parser.nextToken(); + } + for (; t == XContentParser.Token.FIELD_NAME; t = parser.nextToken()) { + String fieldName = parser.currentName(); + t = parser.nextToken(); + Object value = readValue(parser, mapFactory, t); + map.put(fieldName, value); + } + return map; + } + + private static List readList(XContentParser parser, MapFactory mapFactory) throws IOException { + ArrayList list = new ArrayList<>(); + Token t; + while ((t = parser.nextToken()) != XContentParser.Token.END_ARRAY) { + list.add(readValue(parser, mapFactory, t)); + } + return list; + } + + private static Object readValue(XContentParser parser, MapFactory mapFactory, XContentParser.Token t) throws IOException { + if (t == XContentParser.Token.VALUE_NULL) { + return null; + } else if (t == XContentParser.Token.VALUE_STRING) { + if (parser.isBase16Checks()) { + return XContentHelper.parseBase16(parser.text()); + } + return parser.text(); + } else if (t == XContentParser.Token.VALUE_NUMBER) { + XContentParser.NumberType numberType = parser.numberType(); + if (numberType == XContentParser.NumberType.INT) { + return parser.isLosslessDecimals() ? parser.bigIntegerValue() : parser.intValue(); + } else if (numberType == XContentParser.NumberType.LONG) { + return parser.isLosslessDecimals() ? parser.bigIntegerValue() : parser.longValue(); + } else if (numberType == XContentParser.NumberType.FLOAT) { + return parser.isLosslessDecimals() ? parser.bigDecimalValue() : parser.floatValue(); + } else if (numberType == XContentParser.NumberType.DOUBLE) { + return parser.isLosslessDecimals() ? parser.bigDecimalValue() : parser.doubleValue(); + } else if (numberType == NumberType.BIG_INTEGER) { + return parser.bigIntegerValue(); + } else if (numberType == NumberType.BIG_DECIMAL) { + return parser.bigDecimalValue(); + } + } else if (t == XContentParser.Token.VALUE_BOOLEAN) { + return parser.booleanValue(); + } else if (t == XContentParser.Token.START_OBJECT) { + return readMap(parser, mapFactory); + } else if (t == XContentParser.Token.START_ARRAY) { + return readList(parser, mapFactory); + } else if (t == XContentParser.Token.VALUE_EMBEDDED_OBJECT) { + return parser.binaryValue(); + } + return null; + } } diff --git a/content-core/src/main/java/org/xbib/content/core/MapFactory.java b/content-core/src/main/java/org/xbib/content/core/MapFactory.java new file mode 100644 index 0000000..1fdec95 --- /dev/null +++ b/content-core/src/main/java/org/xbib/content/core/MapFactory.java @@ -0,0 +1,8 @@ +package org.xbib.content.core; + +import java.util.Map; + +@FunctionalInterface +public interface MapFactory { + Map newMap(); +} diff --git a/content-json/src/main/java/org/xbib/content/json/JsonSettingsLoader.java b/content-json/src/main/java/org/xbib/content/json/JsonSettingsLoader.java index c9e3155..c021ec6 100644 --- a/content-json/src/main/java/org/xbib/content/json/JsonSettingsLoader.java +++ b/content-json/src/main/java/org/xbib/content/json/JsonSettingsLoader.java @@ -10,8 +10,6 @@ import java.util.Set; */ public class JsonSettingsLoader extends AbstractSettingsLoader { - private static final Set JSON_SUFFIXES = Set.of("json"); - @Override public XContent content() { return JsonXContent.jsonContent(); @@ -19,7 +17,7 @@ public class JsonSettingsLoader extends AbstractSettingsLoader { @Override public Set suffixes() { - return JSON_SUFFIXES; + return Set.of("json"); } @Override diff --git a/content-json/src/main/java/org/xbib/content/json/JsonXContentParser.java b/content-json/src/main/java/org/xbib/content/json/JsonXContentParser.java index bbe1592..8f73a76 100644 --- a/content-json/src/main/java/org/xbib/content/json/JsonXContentParser.java +++ b/content-json/src/main/java/org/xbib/content/json/JsonXContentParser.java @@ -5,10 +5,13 @@ import com.fasterxml.jackson.core.JsonToken; import org.xbib.content.core.AbstractXContentParser; import org.xbib.content.XContent; import org.xbib.content.XContentParser; +import org.xbib.content.core.MapFactory; import java.io.IOException; import java.math.BigDecimal; import java.math.BigInteger; +import java.util.HashMap; +import java.util.LinkedHashMap; public class JsonXContentParser extends AbstractXContentParser { @@ -53,6 +56,16 @@ public class JsonXContentParser extends AbstractXContentParser { return parser.getCurrentName(); } + @Override + protected MapFactory getMapFactory() { + return HashMap::new; + } + + @Override + protected MapFactory getOrderedMapFactory() { + return LinkedHashMap::new; + } + @Override protected boolean doBooleanValue() throws IOException { return parser.getBooleanValue(); diff --git a/content-settings-datastructures-json/build.gradle b/content-settings-datastructures-json/build.gradle new file mode 100644 index 0000000..b7a1b35 --- /dev/null +++ b/content-settings-datastructures-json/build.gradle @@ -0,0 +1,4 @@ +dependencies { + api project(':content-settings-datastructures') + api "org.xbib:datastructures-json-tiny:${project.property('xbib-datastructures.version')}" +} diff --git a/content-settings-datastructures-json/src/main/java/module-info.java b/content-settings-datastructures-json/src/main/java/module-info.java new file mode 100644 index 0000000..3a2c891 --- /dev/null +++ b/content-settings-datastructures-json/src/main/java/module-info.java @@ -0,0 +1,9 @@ +import org.xbib.content.settings.datastructures.SettingsLoader; +import org.xbib.content.settings.datastructures.json.JsonSettingsLoader; + +module org.xbib.content.settings.datastructures.json { + exports org.xbib.content.settings.datastructures.json; + requires transitive org.xbib.content.settings.datastructures; + requires org.xbib.datastructures.json.tiny; + provides SettingsLoader with JsonSettingsLoader; +} diff --git a/content-settings-datastructures-json/src/main/java/org/xbib/content/settings/datastructures/json/JsonSettingsLoader.java b/content-settings-datastructures-json/src/main/java/org/xbib/content/settings/datastructures/json/JsonSettingsLoader.java new file mode 100644 index 0000000..8503c1c --- /dev/null +++ b/content-settings-datastructures-json/src/main/java/org/xbib/content/settings/datastructures/json/JsonSettingsLoader.java @@ -0,0 +1,24 @@ +package org.xbib.content.settings.datastructures.json; + +import org.xbib.content.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 { + + @Override + public DataStructure dataStructure() { + return new Json(); + } + + @Override + public Set suffixes() { + return Set.of("json"); + } + + @Override + public boolean canLoad(String source) { + return source.indexOf('{') != -1 && source.indexOf('}') != -1; + } +} diff --git a/content-settings-datastructures-json/src/main/java/org/xbib/content/settings/datastructures/json/package-info.java b/content-settings-datastructures-json/src/main/java/org/xbib/content/settings/datastructures/json/package-info.java new file mode 100644 index 0000000..7611111 --- /dev/null +++ b/content-settings-datastructures-json/src/main/java/org/xbib/content/settings/datastructures/json/package-info.java @@ -0,0 +1,4 @@ +/** + * JSON settings with the datastructures package. + */ +package org.xbib.content.settings.datastructures.json; diff --git a/content-settings-datastructures-json/src/main/resources/META-INF.services/org.xbib.content.settings.datastructures.SettingsLoader b/content-settings-datastructures-json/src/main/resources/META-INF.services/org.xbib.content.settings.datastructures.SettingsLoader new file mode 100644 index 0000000..6107384 --- /dev/null +++ b/content-settings-datastructures-json/src/main/resources/META-INF.services/org.xbib.content.settings.datastructures.SettingsLoader @@ -0,0 +1 @@ +org.xbib.content.settings.datastructures.json.JsonSettingsLoader diff --git a/content-settings-datastructures-json/src/test/java/org/xbib/content/settings/datastructures/json/test/JsonSettingsTest.java b/content-settings-datastructures-json/src/test/java/org/xbib/content/settings/datastructures/json/test/JsonSettingsTest.java new file mode 100644 index 0000000..fa03784 --- /dev/null +++ b/content-settings-datastructures-json/src/test/java/org/xbib/content/settings/datastructures/json/test/JsonSettingsTest.java @@ -0,0 +1,67 @@ +package org.xbib.content.settings.datastructures.json.test; + +import org.junit.jupiter.api.Test; +import org.xbib.content.settings.datastructures.Settings; +import org.xbib.content.settings.datastructures.SettingsLoader; +import org.xbib.content.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 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/content-settings-datastructures-json/src/test/java/org/xbib/content/settings/datastructures/json/test/package-info.java b/content-settings-datastructures-json/src/test/java/org/xbib/content/settings/datastructures/json/test/package-info.java new file mode 100644 index 0000000..b7705d9 --- /dev/null +++ b/content-settings-datastructures-json/src/test/java/org/xbib/content/settings/datastructures/json/test/package-info.java @@ -0,0 +1,4 @@ +/** + * Testing JSON settings with the datastructures package. + */ +package org.xbib.content.settings.datastructures.json.test; diff --git a/content-settings-datastructures-json/src/test/resources/org/xbib/content/settings/datastructures/json/test/test-settings.json b/content-settings-datastructures-json/src/test/resources/org/xbib/content/settings/datastructures/json/test/test-settings.json new file mode 100644 index 0000000..f949d38 --- /dev/null +++ b/content-settings-datastructures-json/src/test/resources/org/xbib/content/settings/datastructures/json/test/test-settings.json @@ -0,0 +1,3 @@ +{ + "a": "b" +} diff --git a/content-settings-datastructures-yaml/build.gradle b/content-settings-datastructures-yaml/build.gradle new file mode 100644 index 0000000..237a0b9 --- /dev/null +++ b/content-settings-datastructures-yaml/build.gradle @@ -0,0 +1,4 @@ +dependencies { + api project(':content-settings-datastructures') + api "org.xbib:datastructures-yaml-tiny:${project.property('xbib-datastructures.version')}" +} diff --git a/content-settings-datastructures-yaml/src/main/java/module-info.java b/content-settings-datastructures-yaml/src/main/java/module-info.java new file mode 100644 index 0000000..54ed1ec --- /dev/null +++ b/content-settings-datastructures-yaml/src/main/java/module-info.java @@ -0,0 +1,9 @@ +import org.xbib.content.settings.datastructures.SettingsLoader; +import org.xbib.content.settings.datastructures.yaml.YamlSettingsLoader; + +module org.xbib.content.settings.datastructures.yaml { + exports org.xbib.content.settings.datastructures.yaml; + requires transitive org.xbib.content.settings.datastructures; + requires org.xbib.datastructures.yaml.tiny; + provides SettingsLoader with YamlSettingsLoader; +} diff --git a/content-settings-datastructures-yaml/src/main/java/org/xbib/content/settings/datastructures/yaml/YamlSettingsLoader.java b/content-settings-datastructures-yaml/src/main/java/org/xbib/content/settings/datastructures/yaml/YamlSettingsLoader.java new file mode 100644 index 0000000..254a5c8 --- /dev/null +++ b/content-settings-datastructures-yaml/src/main/java/org/xbib/content/settings/datastructures/yaml/YamlSettingsLoader.java @@ -0,0 +1,33 @@ +package org.xbib.content.settings.datastructures.yaml; + +import org.xbib.content.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 { + + @Override + public DataStructure dataStructure() { + return new Yaml(); + } + + @Override + public Set suffixes() { + return Set.of("yml", "yaml"); + } + + @Override + public boolean canLoad(String source) { + return source.indexOf(':') != -1; + } + + @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/content-settings-datastructures-yaml/src/main/java/org/xbib/content/settings/datastructures/yaml/package-info.java b/content-settings-datastructures-yaml/src/main/java/org/xbib/content/settings/datastructures/yaml/package-info.java new file mode 100644 index 0000000..b73226e --- /dev/null +++ b/content-settings-datastructures-yaml/src/main/java/org/xbib/content/settings/datastructures/yaml/package-info.java @@ -0,0 +1,4 @@ +/** + * YAML settings with the datastructures package. + */ +package org.xbib.content.settings.datastructures.yaml; diff --git a/content-settings-datastructures-yaml/src/main/resources/META-INF/services/org.xbib.content.settings.datastructures.SettingsLoader b/content-settings-datastructures-yaml/src/main/resources/META-INF/services/org.xbib.content.settings.datastructures.SettingsLoader new file mode 100644 index 0000000..df7530c --- /dev/null +++ b/content-settings-datastructures-yaml/src/main/resources/META-INF/services/org.xbib.content.settings.datastructures.SettingsLoader @@ -0,0 +1 @@ +org.xbib.content.settings.datastructures.yaml.YamlSettingsLoader diff --git a/content-settings-datastructures-yaml/src/test/java/org/xbib/content/settings/datastructures/yaml/test/YamlSettingsTest.java b/content-settings-datastructures-yaml/src/test/java/org/xbib/content/settings/datastructures/yaml/test/YamlSettingsTest.java new file mode 100644 index 0000000..52afb06 --- /dev/null +++ b/content-settings-datastructures-yaml/src/test/java/org/xbib/content/settings/datastructures/yaml/test/YamlSettingsTest.java @@ -0,0 +1,67 @@ +package org.xbib.content.settings.datastructures.yaml.test; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +import org.junit.jupiter.api.Test; +import org.xbib.content.settings.datastructures.Settings; +import org.xbib.content.settings.datastructures.SettingsLoader; +import org.xbib.content.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/content-settings-datastructures-yaml/src/test/java/org/xbib/content/settings/datastructures/yaml/test/package-info.java b/content-settings-datastructures-yaml/src/test/java/org/xbib/content/settings/datastructures/yaml/test/package-info.java new file mode 100644 index 0000000..f31ba1a --- /dev/null +++ b/content-settings-datastructures-yaml/src/test/java/org/xbib/content/settings/datastructures/yaml/test/package-info.java @@ -0,0 +1,4 @@ +/** + * Testing YAML settings with the datastructures package. + */ +package org.xbib.content.settings.datastructures.yaml.test; diff --git a/content-settings-datastructures/build.gradle b/content-settings-datastructures/build.gradle new file mode 100644 index 0000000..3affdcc --- /dev/null +++ b/content-settings-datastructures/build.gradle @@ -0,0 +1,4 @@ +dependencies { + api "org.xbib:datastructures-api:${project.property('xbib-datastructures.version')}" + api "org.xbib:datastructures-tiny:${project.property('xbib-datastructures.version')}" +} diff --git a/content-settings-datastructures/src/main/java/module-info.java b/content-settings-datastructures/src/main/java/module-info.java new file mode 100644 index 0000000..b56ec02 --- /dev/null +++ b/content-settings-datastructures/src/main/java/module-info.java @@ -0,0 +1,11 @@ +import org.xbib.content.settings.datastructures.PropertiesSettingsLoader; +import org.xbib.content.settings.datastructures.SettingsLoader; + +module org.xbib.content.settings.datastructures { + uses SettingsLoader; + provides SettingsLoader with PropertiesSettingsLoader; + exports org.xbib.content.settings.datastructures; + requires org.xbib.datastructures.tiny; + requires transitive org.xbib.datastructures.api; + requires transitive java.sql; +} diff --git a/content-settings-datastructures/src/main/java/org/xbib/content/settings/datastructures/AbstractSettingsLoader.java b/content-settings-datastructures/src/main/java/org/xbib/content/settings/datastructures/AbstractSettingsLoader.java new file mode 100644 index 0000000..47e419f --- /dev/null +++ b/content-settings-datastructures/src/main/java/org/xbib/content/settings/datastructures/AbstractSettingsLoader.java @@ -0,0 +1,76 @@ +package org.xbib.content.settings.datastructures; + +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 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/content-config/src/main/java/org/xbib/content/config/ExceptionFormatter.java b/content-settings-datastructures/src/main/java/org/xbib/content/settings/datastructures/ExceptionFormatter.java similarity index 96% rename from content-config/src/main/java/org/xbib/content/config/ExceptionFormatter.java rename to content-settings-datastructures/src/main/java/org/xbib/content/settings/datastructures/ExceptionFormatter.java index cfa64b1..90a944e 100644 --- a/content-config/src/main/java/org/xbib/content/config/ExceptionFormatter.java +++ b/content-settings-datastructures/src/main/java/org/xbib/content/settings/datastructures/ExceptionFormatter.java @@ -1,4 +1,4 @@ -package org.xbib.content.config; +package org.xbib.content.settings.datastructures; import java.io.PrintWriter; import java.io.StringWriter; diff --git a/content-settings-datastructures/src/main/java/org/xbib/content/settings/datastructures/PlaceholderResolver.java b/content-settings-datastructures/src/main/java/org/xbib/content/settings/datastructures/PlaceholderResolver.java new file mode 100644 index 0000000..578b79f --- /dev/null +++ b/content-settings-datastructures/src/main/java/org/xbib/content/settings/datastructures/PlaceholderResolver.java @@ -0,0 +1,16 @@ +package org.xbib.content.settings.datastructures; + +/** + * 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/content-settings-datastructures/src/main/java/org/xbib/content/settings/datastructures/PropertiesSettingsLoader.java b/content-settings-datastructures/src/main/java/org/xbib/content/settings/datastructures/PropertiesSettingsLoader.java new file mode 100644 index 0000000..f00a51a --- /dev/null +++ b/content-settings-datastructures/src/main/java/org/xbib/content/settings/datastructures/PropertiesSettingsLoader.java @@ -0,0 +1,49 @@ +package org.xbib.content.settings.datastructures; + +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 { + + @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(); + } + + @Override + public boolean canLoad(String source) { + return true; + } +} diff --git a/content-settings-datastructures/src/main/java/org/xbib/content/settings/datastructures/PropertyPlaceholder.java b/content-settings-datastructures/src/main/java/org/xbib/content/settings/datastructures/PropertyPlaceholder.java new file mode 100644 index 0000000..4f3efa4 --- /dev/null +++ b/content-settings-datastructures/src/main/java/org/xbib/content/settings/datastructures/PropertyPlaceholder.java @@ -0,0 +1,118 @@ +package org.xbib.content.settings.datastructures; + +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) { + 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/content-settings-datastructures/src/main/java/org/xbib/content/settings/datastructures/Settings.java b/content-settings-datastructures/src/main/java/org/xbib/content/settings/datastructures/Settings.java new file mode 100644 index 0000000..17d7ec6 --- /dev/null +++ b/content-settings-datastructures/src/main/java/org/xbib/content/settings/datastructures/Settings.java @@ -0,0 +1,656 @@ +package org.xbib.content.settings.datastructures; + +import org.xbib.datastructures.api.ByteSizeValue; +import org.xbib.datastructures.api.TimeValue; +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.time.DateTimeException; +import java.time.LocalDate; +import java.time.format.DateTimeFormatter; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; + +public class Settings implements AutoCloseable { + + public static final Settings EMPTY_SETTINGS = new Builder().build(); + + public static final String[] EMPTY_ARRAY = new String[0]; + + private final TinyMap map; + + private Settings(TinyMap map) { + this.map = map; + } + + public static Settings fromMap(Map map) { + Builder builder = new Builder(); + for (Map.Entry entry : map.entrySet()) { + builder.put(entry.getKey(), entry.getValue() != null ? entry.getValue().toString() : null); + } + return builder.build(); + } + + public static void toMap(Settings settings, Map map) { + for (String key : settings.getAsMap().keySet()) { + map.put(key, settings.get(key)); + } + } + + /** + * Returns a builder to be used in order to build settings. + * @return a builder + */ + public static Builder settingsBuilder() { + return new Builder(); + } + + public static String[] splitStringByCommaToArray(final String s) { + return splitStringToArray(s, ','); + } + + public static String[] splitStringToArray(final String s, final 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; + } + + public Map getAsMap() { + return this.map; + } + + public Map getAsStructuredMap() { + TinyMap.Builder stringObjectMap = TinyMap.builder(); + for (String key : map.keySet()) { + String value = map.get(key); + processSetting(stringObjectMap, "", key, value); + } + for (String key : stringObjectMap.keySet()) { + Object object = stringObjectMap.get(key); + if (object instanceof Map) { + @SuppressWarnings("unchecked") + Map valMap = (Map) object; + stringObjectMap.put(key, convertMapsToArrays(valMap)); + } + } + return stringObjectMap.build(); + } + + public Settings getByPrefix(String prefix) { + Builder builder = new Builder(); + for (String key : map.keySet()) { + String value = map.get(key); + if (key.startsWith(prefix)) { + if (key.length() < prefix.length()) { + continue; + } + builder.put(key.substring(prefix.length()), value); + } + } + return builder.build(); + } + + public Settings getAsSettings(String setting) { + return getByPrefix(setting + "."); + } + + public boolean containsSetting(String setting) { + if (map.containsKey(setting)) { + return true; + } + for (String key : map.keySet()) { + if (key.startsWith(setting)) { + return true; + } + } + return false; + } + + public String get(String setting) { + return map.get(setting); + } + + public String get(String setting, String defaultValue) { + String s = map.get(setting); + return s == null ? defaultValue : s; + } + + 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); + } + } + + 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); + } + } + + public Integer getAsInt(String setting, Integer 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); + } + } + + 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); + } + } + + 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)); + } + + public TimeValue getAsTime(String setting, TimeValue defaultValue) { + return TimeValue.parseTimeValue(get(setting), defaultValue); + } + + public ByteSizeValue getAsBytesSize(String setting, ByteSizeValue defaultValue) { + return ByteSizeValue.parseBytesSizeValue(get(setting), defaultValue); + } + + public String[] getAsArray(String settingPrefix) { + return getAsArray(settingPrefix, EMPTY_ARRAY); + } + + 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]); + } + + 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 (String key : hashMap.keySet()) { + TinyMap.Builder value = hashMap.get(key); + retVal.put(key, new Settings(value.build())); + } + return retVal.build(); + } + + @Override + public boolean equals(Object o) { + return this == o || !(o == null || getClass() != o.getClass()) && map.equals(((Settings) 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 (String k : innerMap.keySet()) { + Object v = innerMap.get(k); + 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 (String key : map.keySet()) { + Object value = map.get(key); + 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() { + } + + /** + * + */ + public static class Builder { + + private final SettingsLoaderService settingsLoaderService = SettingsLoaderService.getInstance(); + + private final TinyMap.Builder map; + + private Builder() { + map = TinyMap.builder(); + } + + public Map map() { + return map; + } + + public String remove(String key) { + return map.remove(key); + } + + public String get(String key) { + return map.get(key); + } + + 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 + */ + public Builder put(String key, String value) { + map.put(key, value); + return this; + } + + /** + * Sets a setting with the provided setting key and class as value. + * + * @param key The setting key + * @param clazz The setting class value + * @return The builder + */ + public Builder put(String key, Class clazz) { + map.put(key, clazz.getName()); + 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 + */ + public Builder 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 + */ + public Builder 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 + */ + public Builder 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 + */ + public Builder 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 + */ + public Builder 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 + */ + public Builder 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 + */ + public Builder 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 + */ + public Builder 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 + */ + public Builder put(Settings settings) { + map.putAll(settings.getAsMap()); + return this; + } + + /** + * Sets all the provided settings. + * + * @param settings settings + * @return a builder + */ + public Builder put(Map settings) { + map.putAll(settings); + return this; + } + + /** + * Loads settings from the actual string content that represents them using the + * {@link SettingsLoaderService#loaderFromString(String)}. + * + * @param source source + * @return builder + */ + public Builder loadFromString(String source) { + SettingsLoader settingsLoader = settingsLoaderService.loaderFromString(source); + try { + Map loadedSettings = settingsLoader.load(source); + put(loadedSettings); + } catch (Exception e) { + throw new SettingsException("failed to load settings from [" + source + "]", e); + } + return this; + } + + /** + * Loads settings from a resource. + * @param resourceName resource name + * @param inputStream input stream + * @return builder + */ + public Builder loadFromResource(String resourceName, InputStream inputStream) throws SettingsException { + 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; + } + + /** + * Load system properties to this settings. + * @return builder + */ + public Builder 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 + */ + public Builder loadFromSystemEnvironment() { + for (Map.Entry entry : System.getenv().entrySet()) { + put(entry.getKey(), entry.getValue()); + } + return this; + } + + public Builder replacePropertyPlaceholders(PropertyPlaceholder propertyPlaceholder, + PlaceholderResolver placeholderResolver) { + map.replaceAll((k, v) -> propertyPlaceholder.replacePlaceholders(v, placeholderResolver)); + return this; + } + + /** + * 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. + * @return builder + */ + public Builder 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); + } + } + ); + } + + public Settings build() { + return new Settings(map.build()); + } + } +} diff --git a/content-settings-datastructures/src/main/java/org/xbib/content/settings/datastructures/SettingsException.java b/content-settings-datastructures/src/main/java/org/xbib/content/settings/datastructures/SettingsException.java new file mode 100644 index 0000000..9de33e4 --- /dev/null +++ b/content-settings-datastructures/src/main/java/org/xbib/content/settings/datastructures/SettingsException.java @@ -0,0 +1,16 @@ +package org.xbib.content.settings.datastructures; + +/** + * 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/content-settings-datastructures/src/main/java/org/xbib/content/settings/datastructures/SettingsLoader.java b/content-settings-datastructures/src/main/java/org/xbib/content/settings/datastructures/SettingsLoader.java new file mode 100644 index 0000000..372d004 --- /dev/null +++ b/content-settings-datastructures/src/main/java/org/xbib/content/settings/datastructures/SettingsLoader.java @@ -0,0 +1,30 @@ +package org.xbib.content.settings.datastructures; + +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 + * @throws IOException if load fails + */ + Map load(String source) throws IOException; + + Map load(Map source) throws IOException; + + boolean canLoad(String source); +} diff --git a/content-settings-datastructures/src/main/java/org/xbib/content/settings/datastructures/SettingsLoaderService.java b/content-settings-datastructures/src/main/java/org/xbib/content/settings/datastructures/SettingsLoaderService.java new file mode 100644 index 0000000..7f397c2 --- /dev/null +++ b/content-settings-datastructures/src/main/java/org/xbib/content/settings/datastructures/SettingsLoaderService.java @@ -0,0 +1,85 @@ +package org.xbib.content.settings.datastructures; + +import java.sql.Connection; +import java.sql.PreparedStatement; +import java.sql.ResultSet; +import java.sql.SQLException; +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()); + } + + /** + * Returns a {@link SettingsLoader} based on the actual source. + * @param source the source + * @return the settings loader + */ + public SettingsLoader loaderFromString(String source) { + for (SettingsLoader loader : settingsLoaderMap.values()) { + if (loader.canLoad(source)) { + return loader; + } + } + throw new IllegalArgumentException("no settings loader"); + } + + public Set getSuffixes() { + Set suffixes = new HashSet<>(); + for (Set set : settingsLoaderMap.keySet()) { + suffixes.addAll(set); + } + return suffixes; + } + + public static Settings fromJdbcConfTable(Connection connection, String id, String type) throws SQLException { + Settings.Builder settingsBuilder = Settings.settingsBuilder(); + try (PreparedStatement statement = connection.prepareStatement("select key, value from conf where id = ? and type = ?", + new String[]{id, type}); ResultSet resultSet = statement.executeQuery()) { + while (resultSet.next()) { + String key = resultSet.getString("key"); + String value = resultSet.getString("value"); + settingsBuilder.put(key, value); + } + } + return settingsBuilder.build(); + } +} diff --git a/content-settings-datastructures/src/main/java/org/xbib/content/settings/datastructures/package-info.java b/content-settings-datastructures/src/main/java/org/xbib/content/settings/datastructures/package-info.java new file mode 100644 index 0000000..e16581a --- /dev/null +++ b/content-settings-datastructures/src/main/java/org/xbib/content/settings/datastructures/package-info.java @@ -0,0 +1,4 @@ +/** + * Classes for settings using the datastructures API. + */ +package org.xbib.content.settings.datastructures; diff --git a/content-settings-datastructures/src/main/resources/META-INF/services/org.xbib.content.settings.datastructures.SettingsLoader b/content-settings-datastructures/src/main/resources/META-INF/services/org.xbib.content.settings.datastructures.SettingsLoader new file mode 100644 index 0000000..0a2c1e8 --- /dev/null +++ b/content-settings-datastructures/src/main/resources/META-INF/services/org.xbib.content.settings.datastructures.SettingsLoader @@ -0,0 +1 @@ +org.xbib.content.settings.datastructures.PropertiesSettingsLoader diff --git a/content-settings-datastructures/src/test/java/org/xbib/content/settings/datastructures/test/SettingsTest.java b/content-settings-datastructures/src/test/java/org/xbib/content/settings/datastructures/test/SettingsTest.java new file mode 100644 index 0000000..f678623 --- /dev/null +++ b/content-settings-datastructures/src/test/java/org/xbib/content/settings/datastructures/test/SettingsTest.java @@ -0,0 +1,115 @@ +package org.xbib.content.settings.datastructures.test; + +import org.junit.jupiter.api.Test; +import org.xbib.content.settings.datastructures.Settings; + +import java.io.ByteArrayInputStream; +import java.nio.charset.StandardCharsets; +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.assertTrue; + +public class SettingsTest { + + @Test + public void testEmpty() { + Settings settings = Settings.EMPTY_SETTINGS; + assertTrue(settings.getAsMap().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(); + assertTrue(Integer.parseInt(settings.get("date")) > 2000); + } + + @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 testPropertiesLoader() { + Settings settings = Settings.settingsBuilder() + .loadFromResource(".properties", new ByteArrayInputStream("a.b=c".getBytes(StandardCharsets.UTF_8))) + .build(); + assertEquals("{a.b=c}", settings.getAsMap().toString()); + } + +} diff --git a/content-settings/build.gradle b/content-settings/build.gradle index f4477f8..b3118db 100644 --- a/content-settings/build.gradle +++ b/content-settings/build.gradle @@ -1,4 +1,5 @@ dependencies { api project(':content-core') + api "org.xbib:datastructures-tiny:${project.property('xbib-datastructures.version')}" testImplementation project(":content-json") } diff --git a/content-settings/src/main/java/module-info.java b/content-settings/src/main/java/module-info.java index 87d9f38..ae72259 100644 --- a/content-settings/src/main/java/module-info.java +++ b/content-settings/src/main/java/module-info.java @@ -1,4 +1,6 @@ module org.xbib.content.settings { + uses org.xbib.content.SettingsLoader; exports org.xbib.content.settings; requires org.xbib.content.core; + requires transitive org.xbib.datastructures.tiny; } diff --git a/content-settings/src/main/java/org/xbib/content/settings/AbstractSettingsLoader.java b/content-settings/src/main/java/org/xbib/content/settings/AbstractSettingsLoader.java index d8e05a0..f20cdfc 100644 --- a/content-settings/src/main/java/org/xbib/content/settings/AbstractSettingsLoader.java +++ b/content-settings/src/main/java/org/xbib/content/settings/AbstractSettingsLoader.java @@ -79,7 +79,6 @@ public abstract class AbstractSettingsLoader implements SettingsLoader { if (objFieldName != null) { path.add(objFieldName); } - String currentFieldName = null; XContentParser.Token token; while ((token = parser.nextToken()) != XContentParser.Token.END_OBJECT) { diff --git a/content-smile/src/main/java/org/xbib/content/smile/SmileXContentParser.java b/content-smile/src/main/java/org/xbib/content/smile/SmileXContentParser.java index 768ca7a..fdd13a2 100644 --- a/content-smile/src/main/java/org/xbib/content/smile/SmileXContentParser.java +++ b/content-smile/src/main/java/org/xbib/content/smile/SmileXContentParser.java @@ -6,9 +6,13 @@ import com.fasterxml.jackson.dataformat.smile.SmileParser; import org.xbib.content.XContent; import org.xbib.content.XContentParser; import org.xbib.content.core.AbstractXContentParser; +import org.xbib.content.core.MapFactory; + import java.io.IOException; import java.math.BigDecimal; import java.math.BigInteger; +import java.util.HashMap; +import java.util.LinkedHashMap; public class SmileXContentParser extends AbstractXContentParser { @@ -53,6 +57,16 @@ public class SmileXContentParser extends AbstractXContentParser { return parser.getCurrentName(); } + @Override + protected MapFactory getMapFactory() { + return HashMap::new; + } + + @Override + protected MapFactory getOrderedMapFactory() { + return LinkedHashMap::new; + } + @Override protected boolean doBooleanValue() throws IOException { return parser.getBooleanValue(); diff --git a/content-xml/src/main/java/org/xbib/content/xml/XmlXContentParser.java b/content-xml/src/main/java/org/xbib/content/xml/XmlXContentParser.java index 09e34d0..9454981 100644 --- a/content-xml/src/main/java/org/xbib/content/xml/XmlXContentParser.java +++ b/content-xml/src/main/java/org/xbib/content/xml/XmlXContentParser.java @@ -5,10 +5,13 @@ import com.fasterxml.jackson.core.JsonToken; import org.xbib.content.core.AbstractXContentParser; import org.xbib.content.XContent; import org.xbib.content.XContentParser; +import org.xbib.content.core.MapFactory; import java.io.IOException; import java.math.BigDecimal; import java.math.BigInteger; +import java.util.HashMap; +import java.util.LinkedHashMap; /** * @@ -56,6 +59,16 @@ public class XmlXContentParser extends AbstractXContentParser { return parser.getCurrentName(); } + @Override + protected MapFactory getMapFactory() { + return HashMap::new; + } + + @Override + protected MapFactory getOrderedMapFactory() { + return LinkedHashMap::new; + } + @Override protected boolean doBooleanValue() throws IOException { return parser.getBooleanValue(); diff --git a/content-yaml/src/main/java/org/xbib/content/yaml/YamlXContent.java b/content-yaml/src/main/java/org/xbib/content/yaml/YamlXContent.java index 6dfb068..00cd87a 100644 --- a/content-yaml/src/main/java/org/xbib/content/yaml/YamlXContent.java +++ b/content-yaml/src/main/java/org/xbib/content/yaml/YamlXContent.java @@ -53,32 +53,27 @@ public class YamlXContent implements XContent { return new YamlXContentGenerator(yamlFactory.createGenerator(os, JsonEncoding.UTF8)); } - @Override public XContentGenerator createGenerator(Writer writer) throws IOException { return new YamlXContentGenerator(yamlFactory.createGenerator(writer)); } - @Override public XContentParser createParser(String content) throws IOException { return new YamlXContentParser(yamlFactory .createParser(content.getBytes(StandardCharsets.UTF_8))); } - @Override public XContentParser createParser(InputStream is) throws IOException { return new YamlXContentParser(yamlFactory.createParser(is)); } - @Override public XContentParser createParser(byte[] data) throws IOException { return new YamlXContentParser(yamlFactory.createParser(data)); } - @Override public XContentParser createParser(byte[] data, int offset, int length) throws IOException { return new YamlXContentParser(yamlFactory.createParser(data, offset, length)); diff --git a/content-yaml/src/main/java/org/xbib/content/yaml/YamlXContentParser.java b/content-yaml/src/main/java/org/xbib/content/yaml/YamlXContentParser.java index 9879213..068de90 100644 --- a/content-yaml/src/main/java/org/xbib/content/yaml/YamlXContentParser.java +++ b/content-yaml/src/main/java/org/xbib/content/yaml/YamlXContentParser.java @@ -6,9 +6,13 @@ import com.fasterxml.jackson.dataformat.yaml.YAMLParser; import org.xbib.content.XContent; import org.xbib.content.XContentParser; import org.xbib.content.core.AbstractXContentParser; +import org.xbib.content.core.MapFactory; + import java.io.IOException; import java.math.BigDecimal; import java.math.BigInteger; +import java.util.HashMap; +import java.util.LinkedHashMap; public class YamlXContentParser extends AbstractXContentParser { @@ -54,6 +58,16 @@ public class YamlXContentParser extends AbstractXContentParser { return parser.getCurrentName(); } + @Override + protected MapFactory getMapFactory() { + return HashMap::new; + } + + @Override + protected MapFactory getOrderedMapFactory() { + return LinkedHashMap::new; + } + @Override protected boolean doBooleanValue() throws IOException { return parser.getBooleanValue(); diff --git a/gradle.properties b/gradle.properties index 53c77e9..e084716 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,10 +1,10 @@ group = org.xbib name = content -version = 3.0.0 +version = 4.0.0 gradle.wrapper.version = 6.6.1 xbib.net.version = 2.1.1 -xbib-datastructures-tiny.version = 0.1.0 +xbib-datastructures.version = 1.0.0 jackson.version = 2.12.3 woodstox.version = 6.2.6 snakeyaml.version = 1.28 diff --git a/gradle/compile/java.gradle b/gradle/compile/java.gradle index 9c8798d..6ed401e 100644 --- a/gradle/compile/java.gradle +++ b/gradle/compile/java.gradle @@ -28,6 +28,7 @@ task sourcesJar(type: Jar, dependsOn: classes) { task javadocJar(type: Jar, dependsOn: javadoc) { classifier 'javadoc' + from javadoc.destinationDir } artifacts { diff --git a/gradle/test/junit5.gradle b/gradle/test/junit5.gradle index ee630fd..d3305ec 100644 --- a/gradle/test/junit5.gradle +++ b/gradle/test/junit5.gradle @@ -1,5 +1,5 @@ -def junitVersion = project.hasProperty('junit.version')?project.property('junit.version'):'5.6.2' +def junitVersion = project.hasProperty('junit.version')?project.property('junit.version'):'5.7.2' def hamcrestVersion = project.hasProperty('hamcrest.version')?project.property('hamcrest.version'):'2.2' dependencies { diff --git a/settings.gradle b/settings.gradle index e72b640..b0896af 100644 --- a/settings.gradle +++ b/settings.gradle @@ -10,3 +10,6 @@ include 'content-settings' include 'content-smile' include 'content-xml' include 'content-yaml' +include 'content-settings-datastructures' +include 'content-settings-datastructures-json' +include 'content-settings-datastructures-yaml'