diff --git a/README.md b/README.md index a864c18..2cd356a 100644 --- a/README.md +++ b/README.md @@ -41,40 +41,41 @@ the [Configuration properties](#configuration-properties) section. ```java public final class Example { - // To create a configuration annotate the class with @Configuration and make sure that - // it has a no-args constructor. That's it! Now you can add fields to it which can all - // be private; setters are not required! + // * To create a configuration annotate a class with @Configuration and make sure that + // it has a no-args constructor. + // * Now add fields to that class and assign them default values. + // * That's it! Fields can be private; setters are not required. @Configuration public static class BaseConfiguration { private String host = "127.0.0.1"; private int port = 1234; // The library supports lists, sets, and maps. - private Set blockedAddresses = Set.of(); + private Set blockedAddresses = Set.of("8.8.8.8"); // Fields can be ignored by making them final, transient, static or by // annotating them with @Ignore. private final double ignoreMe = 3.14; } - // This class does not need to be annotated with @Configuration because it - // extends a class which already is! + // This library supports records; no @Configuration annotation required + public record User( + String username, + @Comment("Please choose a strong password.") + String password + ) {} + + // Subclassing of configurations and nesting of configurations in other configurations + // is also supported. Subclasses don't need to be annotated again. public static final class UserConfiguration extends BaseConfiguration { // You can add comments with the @Comment annotation. Each string in the comment // array is written (as a comment) on a new line. @Comment({"The admin user has full access.", "Choose a proper password!"}) - User admin = new User("root", "toor"); // The User class is a @Configuration! + User admin = new User("root", "toor"); // The User class is a record! List blockedUsers = List.of( new User("user1", null), // null values are supported new User("user2", null) ); } - // This library supports records; no @Configuration annotation required - public record User( - String username, - @Comment("Please choose a strong password.") - String password - ) {} - public static void main(String[] args) { var configFile = Paths.get("/tmp/config.yml"); var config = new UserConfiguration(); @@ -100,7 +101,8 @@ looks like this: ```yaml host: 127.0.0.1 port: 1234 -blockedAddresses: [ ] +blockedAddresses: + - 8.8.8.8 # The admin user has full access. # Choose a proper password! admin: diff --git a/configlib-core/src/main/java/de/exlll/configlib/Configurations.java b/configlib-core/src/main/java/de/exlll/configlib/Configurations.java index 8c07323..3407fc4 100644 --- a/configlib-core/src/main/java/de/exlll/configlib/Configurations.java +++ b/configlib-core/src/main/java/de/exlll/configlib/Configurations.java @@ -85,7 +85,7 @@ public final class Configurations { * Updates a YAML configuration file with a configuration of the given type using a * {@code YamlConfigurationProperties} object with default values. *

- * See {@link YamlConfigurationStore#save(Object, Path)} for an explanation of how the update is + * See {@link YamlConfigurationStore#save(Object, Path)} for an explanation of how the update is // TODO: change #save to #update * done. * * @param configurationFile the configuration file that is updated diff --git a/configlib-core/src/main/java/de/exlll/configlib/FileConfigurationStore.java b/configlib-core/src/main/java/de/exlll/configlib/FileConfigurationStore.java index 0a71abf..3d0e47c 100644 --- a/configlib-core/src/main/java/de/exlll/configlib/FileConfigurationStore.java +++ b/configlib-core/src/main/java/de/exlll/configlib/FileConfigurationStore.java @@ -45,6 +45,25 @@ public interface FileConfigurationStore { * @throws ConfigurationException if the configuration cannot be deserialized * @throws NullPointerException if {@code configurationFile} is null * @throws RuntimeException if loading or saving the configuration throws an exception + * @see #update(Path, Object) */ T update(Path configurationFile); + + /** + * Updates the configuration file. If the file does not exist, it is created and populated + * with values taken from the {@code defaultConfiguration} object. + * Otherwise, if the file exists, a new configuration instance is created, initialized with the + * values taken from the configuration file, and immediately saved to reflect possible changes + * of the configuration type. + * + * @param configurationFile the configuration file that is updated + * @param defaultConfiguration the default value used when creating a new file + * @return a configuration initialized with values taken from the configuration file or + * the default value if the file was newly created + * @throws ConfigurationException if the configuration cannot be deserialized + * @throws NullPointerException if any argument is null + * @throws RuntimeException if loading or saving the configuration throws an exception + * @see #update(Path) + */ + T update(Path configurationFile, T defaultConfiguration); } diff --git a/configlib-core/src/main/java/de/exlll/configlib/YamlConfigurationStore.java b/configlib-core/src/main/java/de/exlll/configlib/YamlConfigurationStore.java index 1d4230e..a53386d 100644 --- a/configlib-core/src/main/java/de/exlll/configlib/YamlConfigurationStore.java +++ b/configlib-core/src/main/java/de/exlll/configlib/YamlConfigurationStore.java @@ -29,7 +29,6 @@ import static de.exlll.configlib.Validator.requireNonNull; public final class YamlConfigurationStore implements FileConfigurationStore { private static final Dump YAML_DUMPER = newYamlDumper(); private static final Load YAML_LOADER = newYamlLoader(); - private final Class configurationType; private final YamlConfigurationProperties properties; private final TypeSerializer serializer; private final CommentNodeExtractor extractor; @@ -42,7 +41,7 @@ public final class YamlConfigurationStore implements FileConfigurationStore configurationType, YamlConfigurationProperties properties) { - this.configurationType = requireNonNull(configurationType, "configuration type"); + requireNonNull(configurationType, "configuration type"); this.properties = requireNonNull(properties, "properties"); this.serializer = TypeSerializer.newSerializerFor(configurationType, properties); this.extractor = new CommentNodeExtractor(properties); @@ -111,14 +110,18 @@ public final class YamlConfigurationStore implements FileConfigurationStore store = newDefaultStore(E.class); + final E defaultConfig = new E(20, 21); + + assertFalse(Files.exists(yamlFile)); + E config = store.update(yamlFile, defaultConfig); + assertEquals("i: 20\nj: 21", readFile(yamlFile)); + assertSame(defaultConfig, config); + } + + @Test + void updateWithDefaultCreatesConfigurationFileIfItDoesNotExistRecord() { + record R(int i, char c, String s) {} + + final YamlConfigurationStore store = new YamlConfigurationStore<>( + R.class, + YamlConfigurationProperties.newBuilder().outputNulls(true).build() + ); + final R defaultConfig = new R(1, 'a', "s"); + + assertFalse(Files.exists(yamlFile)); + R config = store.update(yamlFile, defaultConfig); + assertEquals("i: 1\nc: a\ns: s", readFile(yamlFile)); + assertSame(defaultConfig, config); + } + + @Test + void updateWithDefaultLoadsConfigurationFileIfItDoesExist() throws IOException { + final YamlConfigurationStore store = newDefaultStore(E.class); + final E defaultConfig = new E(100, 200); + + Files.writeString(yamlFile, "i: 20"); + E config = store.update(yamlFile, defaultConfig); + assertEquals(20, config.i); + assertEquals(11, config.j); + assertNotSame(defaultConfig, config); + } + + @Test + void updateWithDefaultLoadsConfigurationFileIfItDoesExistRecord() throws IOException { + record R(int i, int j) {} + final YamlConfigurationStore store = newDefaultStore(R.class); + final R defaultConfig = new R(100, 200); + + Files.writeString(yamlFile, "i: 20"); + R config = store.update(yamlFile, defaultConfig); + assertEquals(20, config.i); + assertEquals(0, config.j); + } + + @Test + void updateWithDefaultUpdatesFile() throws IOException { + final YamlConfigurationStore store = newDefaultStore(E.class); + final E defaultConfig = new E(100, 200); + + Files.writeString(yamlFile, "i: 20\nk: 30"); + E config = store.update(yamlFile, defaultConfig); + assertEquals(20, config.i); + assertEquals(11, config.j); + assertEquals("i: 20\nj: 11", readFile(yamlFile)); + } + + @Test + void updateWithDefaultUpdatesFileRecord() throws IOException { + record R(int i, int j) {} + final YamlConfigurationStore store = newDefaultStore(R.class); + final R defaultConfig = new R(100, 200); + + Files.writeString(yamlFile, "i: 20\nk: 30"); + R config = store.update(yamlFile, defaultConfig); + assertEquals(20, config.i); + assertEquals(0, config.j); + assertEquals("i: 20\nj: 0", readFile(yamlFile)); + } + private static YamlConfigurationStore newDefaultStore(Class configType) { YamlConfigurationProperties properties = YamlConfigurationProperties.newBuilder().build(); return new YamlConfigurationStore<>(configType, properties);