From 6ee99ff61952017ccc8fd7ecbdd5b7ee9bcd14e6 Mon Sep 17 00:00:00 2001 From: Exlll Date: Sat, 16 Jul 2022 03:34:48 +0200 Subject: [PATCH] Update documentation for Records --- README.md | 122 +++++++++--------- .../main/java/de/exlll/configlib/Comment.java | 4 +- .../java/de/exlll/configlib/CommentNode.java | 26 ++-- .../exlll/configlib/CommentNodeExtractor.java | 22 ++-- .../configlib/ConfigurationProperties.java | 19 +-- .../configlib/FileConfigurationStore.java | 7 +- .../configlib/YamlConfigurationStore.java | 17 +-- .../configlib/CommentNodeExtractorTest.java | 4 +- 8 files changed, 117 insertions(+), 104 deletions(-) diff --git a/README.md b/README.md index a071619..a864c18 100644 --- a/README.md +++ b/README.md @@ -13,12 +13,12 @@ the [Tutorial](https://github.com/Exlll/ConfigLib/wiki/Tutorial) page on the wik * Automatic creation, saving, loading, and updating of configuration files * Support for comments through annotations -* Support for all primitive types, their wrapper types, and Strings +* Support for all primitive types, their wrapper types, and strings +* Support for enums, records, and POJOs (+ inheritance!) +* Support for (nested) lists, sets, arrays, and maps * Support for `BigInteger` and `BigDecimal` * Support for `LocalDate`, `LocalTime`, `LocalDateTime`, and `Instant` * Support for `UUID`, `File`, `Path`, `URL`, and `URI` -* Support for (nested) lists, sets, arrays, and maps -* Support for enums and POJOs (+ inheritance!) * Support for Bukkit's `ConfigurationSerializable` types (e.g. `ItemStack`) * Option to exclude fields from being converted * Option to format field and component names before conversion @@ -68,18 +68,12 @@ public final class Example { ); } - @Configuration - public static final class User { - private String username; - @Comment("Please choose a strong password.") - private String password; - - // Configuration classes require a no-args constructor. - // The constructor can be private though. - private User() {} - - public User(String username, String password) {/* initialize */} - } + // 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"); @@ -129,25 +123,25 @@ Two things are noticeable here: ## General information -In the following sections the term _configuration type_ refers to any non-generic class that is -directly or indirectly (i.e. through subclassing) annotated with +In the following sections the term _configuration type_ refers to any record type or to any +non-generic class that is directly or indirectly (i.e. through subclassing) annotated with `@de.exlll.configlib.Configuration`. Accordingly, the term _configuration_ refers to an instance of such a type. ### Declaring configuration types -To declare a configuration type, annotate a class with `@Configuration` and make sure that it has a -no-args constructor. The no-args constructor can be set `private`. Inner classes (i.e. the ones that -are nested but not `static`) have an implicit synthetic constructor with at least one argument and -are therefore not supported. +To declare a configuration type, either define a record or annotate a class with `@Configuration` +and make sure that it has a no-args constructor. The no-args constructor can be set `private`. Inner +classes (i.e. the ones that are nested but not `static`) have an implicit synthetic constructor with +at least one argument and are therefore not supported. -Add fields to your class whose type is any of the supported types listed in the next section. You -should initialize all fields of reference types with non-null default values, though you can leave -them null. Handling of null values is discussed in one of the sections further below. +Now simply add components to your record or fields to your class whose type is any of the supported +types listed in the next section. You can (and should) initialize all fields of a configuration +class with non-null default values. ### Supported types -A configuration type may only contain fields of the following types: +A configuration type may only contain fields or components of the following types: | Type class | Types | |-----------------------------|--------------------------------------------------------------------| @@ -159,7 +153,7 @@ A configuration type may only contain fields of the following types: | Time related types | `LocalTime`, `LocalDate`, `LocalDateTime`, `Instant` | | Utility types | `UUID`, `File`, `Path`, `URL`, `URI` | | Enums | Any Java enum | -| Configurations | Any configuration type | +| Configurations | Any Java record or any class annotated with `@Configuration` | | `ConfigurationSerializable` | All Bukkit classes that implement this interface, like `ItemStack` | | Collections | (Nested) Lists, sets, maps*, or arrays of previously listed types | @@ -178,18 +172,19 @@ public final class SupportedTypes { LocalTime supported; UUID supported; ExampleEnum supported; // where 'ExampleEnum' is some Java enum type - ExampleConf supported; // where 'ExampleConf' is another configuration type + ExampleConfig supported; // where 'ExampleConfig' is a class annotated with @Configuration + ExampleRecord supported; // where 'ExampleRecord' is a Java record /* collection types */ List supported; Set supported; LocalDate[] supported; - Map supported; + Map supported; /* nested collection types */ List> supported; int[][] supported; - Map>>> supported; + Map>>> supported; // supported if a custom serializer is registered java.awt.Point supported; @@ -231,8 +226,8 @@ Both ways have three methods in common: * `save` saves a configuration to a file * `load` creates a new configuration instance and populates it with values taken from a file * `update` is a combination of `load` and `save` and the method you'd usually want to use: it takes - care of creating the configuration file if it does not exist and updates it otherwise to reflect - changes to (the fields of) the configuration type. + care of creating the configuration file if it does not exist and otherwise updates it to reflect + changes to (the fields or components of) the configuration type.
Example of update behavior when configuration file exists @@ -347,9 +342,9 @@ described in the [Import](#import) section. ### Comments -The fields of a configuration can be annotated with the `@Comment` annotation. This annotation takes -an array of strings. Each of these strings is written onto a new line as a comment. Empty strings -are written as newlines. +The fields or components of a configuration can be annotated with the `@Comment` annotation. This +annotation takes an array of strings. Each of these strings is written onto a new line as a comment. +Empty strings are written as newlines. Serializing the following configuration as YAML ... @@ -371,9 +366,10 @@ public final class ExampleConfiguration { commentedField: commented field ``` -If a configuration type _C_ that defines comments is used (as a field) within another configuration -type, the comments of _C_ are written with the proper indentation. However, if instances of _C_ are -stored inside a collection, their comments are not printed when the collection is written. +If a configuration type _C_ that defines comments is used (as a field or component) within another +configuration type, the comments of _C_ are written with the proper indentation. However, if +instances of _C_ are stored inside a collection, their comments are not printed when the collection +is written. ### Subclassing @@ -393,7 +389,7 @@ and describe how to handle name clashes.) Fields that are `final`, `static`, `transient` or annotated with `@Ignore` are neither serialized nor updated during deserialization. You can filter out additional fields by providing an instance of -`FieldFilter` to the configuration properties. +`FieldFilter` to the configuration properties. Record components cannot be filtered. ### Handling of missing and `null` values @@ -403,15 +399,20 @@ When a configuration file is read, values that correspond to a field of a config component of a record type might be missing. That can happen, for example, when somebody deleted that field from the configuration file, when the definition of a configuration or record type is changed, or when the `NameFormatter` that was used -to create that file is changed. +to create that file is replaced. In such cases, fields of configuration types keep the default value you assigned to them and record components are initialized with the default value of their corresponding type. -#### `null` values +#### Null values + +**NOTE:** Null values written to a configuration file generally don't give any indication about +which kinds of values the configuration expects. Therefore, they not only make it harder for the +users of that configuration file to properly configure it, but they might also prevent loading a +configuration if the values the users set are of the wrong type. -Configuration properties let you configure how `null` values are handled when serializing and -deserializing a configuration or record type: +Although strongly discouraged, null values are supported and `ConfigurationProperties` let you +configure how they are handled when serializing and deserializing a configuration: * By setting `outputNulls` to false, class fields, record components, and collection elements that are null are not output. Any comments that belong to such fields are also not written. @@ -474,8 +475,8 @@ properties. This also means that `Set`s are valid target types. #### Serializer selection -To convert the value of a field `F` with (source) type `S` into a serializable value of some -target type, a serializer has to be selected. Serializers are instances of +To convert the value of a field or record component `F` with (source) type `S` into a serializable +value of some target type, a serializer has to be selected. Serializers are instances of the `de.exlll.configlib.Serializer` interface and are selected based on `S`. Put differently, serializers are always selected based on the compile-time type of `F` and never on the runtime type of its value. @@ -483,23 +484,23 @@ of its value.
Why should I care about this? -This distinction makes a difference (and might lead to confusion) when you have fields whose type is -a configuration type or a collection of some configuration type, and you extend that configuration -type. Concretely, assume you have and written two configuration types `A` and `B` -where `B extends A`. Then, if you use `A a = new B()` in your main configuration, only the fields of -a `A` will be stored when you save your main configuration. That is because the serializer of -field `a` was selected based on the compile-time type of `a` which is `A` and not `B`. The same -happens if you have a `List` and put instances of `B` (or some other subclass of `A`) in it. +This distinction makes a difference (and might lead to confusion) when you have fields or record +components whose type is a configuration type, and you extend that configuration type. Concretely, +assume you have written two configuration classes `A` and `B` where `B extends A`. Then, if you +use `A a = new B()` in your main configuration, only the fields of a `A` will be stored when you +save your main configuration. That is because the serializer of field `a` was selected based on the +compile-time type of `a` which is `A` and not `B`. The same happens if you have a `List` and put +instances of `B` (or some other subclass of `A`) in it.
#### Custom serializers -If you want to add support for a type whose class is not annotated with `@Configuration`, you can -register a custom serializer. Serializers are instances of the `de.exlll.configlib.Serializer` -interface. When implementing that interface you have to make sure that you convert your source type -into one of the valid target types listed in the table above. The serializer then has to be -registered through a `ConfigurationProperties` object. +If you want to add support for a type is not a record or whose class is not annotated +with `@Configuration`, you can register a custom serializer. Serializers are instances of +the `de.exlll.configlib.Serializer` interface. When implementing that interface you have to make +sure that you convert your source type into one of the valid target types listed in the table above. +The serializer then has to be registered through a `ConfigurationProperties` object. The following `Serializer` serializes instances of `java.awt.Point` into strings. @@ -522,11 +523,12 @@ public final class PointSerializer implements Serializer { Custom serializers takes precedence over the serializers provided by this library. -### Changing the type of fields +### Changing the type of fields or record components -Changing the type of fields is not supported. If you change the type of one of your fields but your -configuration file still contains a value of the old type, a type mismatch will occur when reading -that file. Instead, remove the old field and add a new one with a different name. +Changing the type of fields or record components is not supported. If you change the type of one of +these but your configuration file still contains a value of the old type, a type mismatch will +occur when loading a configuration from that file. Instead, remove the old element and add a new one +with a different name. ### Recursive type definitions diff --git a/configlib-core/src/main/java/de/exlll/configlib/Comment.java b/configlib-core/src/main/java/de/exlll/configlib/Comment.java index ae75f3c..4921955 100644 --- a/configlib-core/src/main/java/de/exlll/configlib/Comment.java +++ b/configlib-core/src/main/java/de/exlll/configlib/Comment.java @@ -13,9 +13,9 @@ import java.lang.annotation.Target; @Retention(RetentionPolicy.RUNTIME) public @interface Comment { /** - * Returns the comments of the annotated field. + * Returns the comments of the annotated field or record component. * - * @return field comments + * @return field or record component comments */ String[] value(); } \ No newline at end of file diff --git a/configlib-core/src/main/java/de/exlll/configlib/CommentNode.java b/configlib-core/src/main/java/de/exlll/configlib/CommentNode.java index 610e656..8a3885d 100644 --- a/configlib-core/src/main/java/de/exlll/configlib/CommentNode.java +++ b/configlib-core/src/main/java/de/exlll/configlib/CommentNode.java @@ -3,28 +3,30 @@ package de.exlll.configlib; import java.util.List; /** - * Holds the comments of a field as well as a list of field names. The list of - * field names contains the names of all fields which led to this field from the - * root of the configuration object. For example, for the following situation the + * Holds the comments of a field or record component as well as a list of element names. + * The list of element names contains the names of all fields or record components which led + * to the current element starting from the root of the configuration object. + *

+ * For example, for the following situation, if an instance of {@code A} is our root, the * {@code CommentNode} of the field {@code fn2} would hold {@code comments} and - * {@code fieldNames} lists that contain the values {@code ["Hello World"]} and - * {@code ["fn0", "fn1", "fn2"]}, respectively. + * {@code elementNames} lists that contain the values {@code ["Hello","World"]} and + * {@code ["fn0","fn1","fn2"]}, respectively. + * *

  * class A {
  *     B fn0 = new B();
  * }
- * class B {
- *     C fn1 = new C();
- * }
+ *
+ * record B(C fn1) {}
+ *
  * class C {
- *     {@code @Comment("Hello world")}
+ *     {@code @Comment({"Hello", "World"})}
  *     int fn2;
  * }
  *  
* * @param comments - * @param fieldNames + * @param elementNames */ -// TODO change javadoc -record CommentNode(List comments, List fieldNames) {} +record CommentNode(List comments, List elementNames) {} diff --git a/configlib-core/src/main/java/de/exlll/configlib/CommentNodeExtractor.java b/configlib-core/src/main/java/de/exlll/configlib/CommentNodeExtractor.java index b619400..88ee271 100644 --- a/configlib-core/src/main/java/de/exlll/configlib/CommentNodeExtractor.java +++ b/configlib-core/src/main/java/de/exlll/configlib/CommentNodeExtractor.java @@ -33,13 +33,13 @@ final class CommentNodeExtractor { public Queue extractCommentNodes(final Object componentHolder) { requireConfigurationOrRecord(componentHolder.getClass()); final Queue result = new ArrayDeque<>(); - final var fnameStack = new ArrayDeque<>(List.of("")); + final var elementNameStack = new ArrayDeque<>(List.of("")); final var stateStack = new ArrayDeque<>(List.of(stateFromObject(componentHolder))); State state; while (!stateStack.isEmpty()) { state = stateStack.removeLast(); - fnameStack.removeLast(); + elementNameStack.removeLast(); while (state.iterator.hasNext()) { final var component = state.iterator.next(); @@ -60,7 +60,11 @@ final class CommentNodeExtractor { assert element != null; final var componentName = component.componentName(); - final var commentNode = createNodeIfCommentPresent(element, componentName, fnameStack); + final var commentNode = createNodeIfCommentPresent( + element, + componentName, + elementNameStack + ); commentNode.ifPresent(result::add); if ((componentValue == null) || @@ -69,7 +73,7 @@ final class CommentNodeExtractor { continue; stateStack.addLast(new State(state.iterator, state.componentHolder)); - fnameStack.addLast(nameFormatter.format(componentName)); + elementNameStack.addLast(nameFormatter.format(componentName)); state = stateFromObject(componentValue); } } @@ -88,14 +92,14 @@ final class CommentNodeExtractor { private Optional createNodeIfCommentPresent( final AnnotatedElement element, final String elementName, - final Deque fileNameStack + final Deque elementNameStack ) { if (element.isAnnotationPresent(Comment.class)) { final var comments = element.getAnnotation(Comment.class).value(); - final var fieldName = nameFormatter.format(elementName); - final var fieldNames = new ArrayList<>(fileNameStack); - fieldNames.add(fieldName); - final var result = new CommentNode(Arrays.asList(comments), fieldNames); + final var formattedName = nameFormatter.format(elementName); + final var elementNames = new ArrayList<>(elementNameStack); + elementNames.add(formattedName); + final var result = new CommentNode(Arrays.asList(comments), elementNames); return Optional.of(result); } return Optional.empty(); diff --git a/configlib-core/src/main/java/de/exlll/configlib/ConfigurationProperties.java b/configlib-core/src/main/java/de/exlll/configlib/ConfigurationProperties.java index 80bd1f0..83fb21e 100644 --- a/configlib-core/src/main/java/de/exlll/configlib/ConfigurationProperties.java +++ b/configlib-core/src/main/java/de/exlll/configlib/ConfigurationProperties.java @@ -144,10 +144,10 @@ class ConfigurationProperties { /** * Adds a serializer for the condition. The serializer is selected when the condition * evaluates to true. The {@code test} method of the condition object is invoked with - * the type of a field. Serializers added by this method take precedence over all other - * serializers expect the ones that were added for a specific type by the - * {@link #addSerializer(Class, Serializer)} method. The conditions are checked in the order - * in which they were added. + * the generic type of a field or record component. Serializers added by this method + * take precedence over all other serializers expect the ones that were added for a + * specific type by the {@link #addSerializer(Class, Serializer)} method. + * The conditions are checked in the order in which they were added. * * @param condition the condition * @param serializer the serializer @@ -165,8 +165,8 @@ class ConfigurationProperties { } /** - * Sets whether fields or collection elements whose value is null should be output - * while serializing the configuration. + * Sets whether fields, record components, or collection elements whose value + * is null should be output while serializing the configuration. *

* The default value is {@code false}. * @@ -179,8 +179,11 @@ class ConfigurationProperties { } /** - * Sets whether fields or collection elements should allow null values to bet set - * while deserializing the configuration. + * Sets whether fields, record components, or collection elements should + * allow null values to bet set while deserializing the configuration. + *

+ * If this option is set to false, null values read from a configuration + * are treated as missing. *

* The default value is {@code false}. * 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 b24adb5..0a71abf 100644 --- a/configlib-core/src/main/java/de/exlll/configlib/FileConfigurationStore.java +++ b/configlib-core/src/main/java/de/exlll/configlib/FileConfigurationStore.java @@ -35,9 +35,10 @@ public interface FileConfigurationStore { /** * Updates the configuration file. If the file does not exist, it is created and populated * with the default values with which the fields of the configuration have been initialized. - * Otherwise, 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. + * If the configuration is of record type, the default values are chosen to be the default + * values of its component types. 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 * @return a newly created configuration initialized with values taken from the configuration file 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 0c8a6b5..1d4230e 100644 --- a/configlib-core/src/main/java/de/exlll/configlib/YamlConfigurationStore.java +++ b/configlib-core/src/main/java/de/exlll/configlib/YamlConfigurationStore.java @@ -211,11 +211,12 @@ public final class YamlConfigurationStore implements FileConfigurationStore implements FileConfigurationStore comments, String... fieldNames) { - return new CommentNode(comments, List.of(fieldNames)); + private static CommentNode cn(List comments, String... elementNames) { + return new CommentNode(comments, List.of(elementNames)); } } \ No newline at end of file