forked from public-mirrors/ConfigLib
You cannot select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
585 lines
22 KiB
Markdown
585 lines
22 KiB
Markdown
# ConfigLib
|
|
|
|
**A Minecraft library for mapping objects to YAML configuration files.**
|
|
|
|
This library facilitates creating, saving, loading, updating, and documenting YAML configuration
|
|
files. It does so by automatically mapping instances of configuration classes to serializable maps
|
|
which are then transformed into YAML and saved to some specified file.
|
|
|
|
## Features
|
|
|
|
* Automatic creation, saving, loading, and updating of configuration files
|
|
* Support for all primitive types, their wrapper types, and Strings
|
|
* Support for `BigInteger`, `BigDecimal`, `LocalDate`, `LocalTime`, and `LocalDateTime`
|
|
* Support for (nested) lists, sets, arrays, and maps
|
|
* Support for enums and POJOs (+ inheritance!)
|
|
* Option to exclude fields from being converted
|
|
* Option to add explanatory comments by annotating fields
|
|
* Option to format field names before conversion
|
|
* Option to customize null handling
|
|
* Option to provide custom serializers
|
|
* ...and a few more!
|
|
|
|
## Usage example
|
|
|
|
This section contains a short usage example to get you started. The whole range of features is
|
|
discussed in the following sections. Information on how to import this library is located at the end
|
|
of this documentation.
|
|
|
|
```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!
|
|
@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<String> blockedAddresses = Set.of();
|
|
// 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!
|
|
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!
|
|
List<User> blockedUsers = List.of(
|
|
new User("user1", null), // null values are supported
|
|
new User("user2", null)
|
|
);
|
|
}
|
|
|
|
@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 */}
|
|
}
|
|
|
|
public static void main(String[] args) {
|
|
final var props = YamlConfigurationProperties.newBuilder().build();
|
|
final var store = new YamlConfigurationStore<>(UserConfiguration.class, props);
|
|
final var configFile = Paths.get("/tmp/config.yml");
|
|
|
|
// Save a new instance to the configuration file
|
|
store.save(new UserConfiguration(), configFile);
|
|
|
|
// Load a new instance from the configuration file
|
|
UserConfiguration configuration = store.load(configFile);
|
|
System.out.println(configuration.admin.username);
|
|
System.out.println(configuration.blockedUsers);
|
|
|
|
// Modify and save the configuration file
|
|
configuration.blockedUsers.add(new User("user3", "pass3"));
|
|
store.save(configuration, configFile);
|
|
}
|
|
}
|
|
```
|
|
|
|
By running the above code, a new YAML configuration is created at `/tmp/config.yml`. Its content
|
|
looks like this:
|
|
|
|
```yaml
|
|
host: 127.0.0.1
|
|
port: 1234
|
|
blockedAddresses: [ ]
|
|
# The admin user has full access.
|
|
# Choose a proper password!
|
|
admin:
|
|
username: root
|
|
# Please choose a strong password.
|
|
password: toor
|
|
blockedUsers:
|
|
- username: user1
|
|
- username: user2
|
|
- username: user3
|
|
password: pass3
|
|
```
|
|
|
|
Two things are noticeable here:
|
|
|
|
1. Not every user in the `blockedUsers` list has a `password` mapping. This is because null values
|
|
are not output by default. That behavior can be changed by the builder.
|
|
2. The password of the user with username `user3` that has no comment. This is due to limitations of
|
|
the YAML library. Configurations in lists, sets, or maps cannot have their comments printed.
|
|
|
|
## 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
|
|
`@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.
|
|
|
|
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.
|
|
|
|
### Supported types
|
|
|
|
A configuration type may only contain fields of the following types:
|
|
|
|
| Type class | Types |
|
|
|------------------------|--------------------------------------------------------------------|
|
|
| Boolean types | `boolean`, and `Boolean` |
|
|
| Integer types | `byte`, `short`, `int`, `long`, and their respective wrapper types |
|
|
| Floating point types | `float`, `double`, and their respective wrapper types |
|
|
| Characters and strings | `char`, `Character`, `String` |
|
|
| Big numeric types | `BigInteger`, `BigDecimal` |
|
|
| Time related types | `LocalTime`, `LocalDate`, `LocalDateTime` |
|
|
| Enums | Any Java enum |
|
|
| Configurations | Any configuration type |
|
|
| Collections | (Nested) Lists, sets, maps*, or arrays of previously listed types |
|
|
|
|
(*) Map keys cannot be in the `Configurations` or `Collections` type class.
|
|
|
|
|
|
<details>
|
|
<summary>Examples of supported types</summary>
|
|
|
|
The following class contains examples of types that this library supports:
|
|
|
|
```java
|
|
public final class SupportedTypes {
|
|
boolean supported;
|
|
Character supported;
|
|
ExampleEnum supported; // where 'ExampleEnum' is some Java enum type
|
|
ExampleConf supported; // where 'ExampleConf' is another configuration type
|
|
java.awt.Point supported; // only if a custom serializer is registered
|
|
/* collection types */
|
|
List<BigInteger> supported;
|
|
Set<Double> supported;
|
|
LocalDate[] supported;
|
|
Map<ExampleEnum, ExampleConf> supported;
|
|
/* nested collection types */
|
|
List<Map<ExampleEnum, LocalDate>> supported;
|
|
int[][] supported;
|
|
Map<Integer, List<Map<Short, Set<ExampleConf>>>> supported;
|
|
}
|
|
```
|
|
|
|
</details>
|
|
|
|
<details>
|
|
<summary>Examples of unsupported types</summary>
|
|
|
|
The following class contains examples of types that this library does (and will) not support:
|
|
|
|
```java
|
|
public final class UnsupportedTypes<T> {
|
|
Map<Point, String> unsupported; // invalid map key
|
|
Map<List<String>, String> unsupported; // invalid map key
|
|
Box<String> unsupported; // custom parameterized type
|
|
List<? extends String> unsupported; // wildcard type
|
|
List<?> unsupported; // wildcard type
|
|
List<?>[] unsupported; // wildcard type
|
|
T unsupported; // type variable
|
|
List unsupported; // raw type
|
|
List[] unsupported; // raw type
|
|
List<String>[] unsupported; // generic array type
|
|
Set<Integer>[] unsupported; // generic array type
|
|
Map<Byte, Byte>[] unsupported; // generic array type
|
|
}
|
|
```
|
|
|
|
</details>
|
|
|
|
### Loading and saving configurations
|
|
|
|
There are two ways to load and save configurations. Which way you choose depends on your liking.
|
|
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.
|
|
|
|
<details>
|
|
<summary>Example of <code>update</code> behavior when configuration file exists</summary>
|
|
|
|
Let's say you have the following configuration type:
|
|
|
|
```java
|
|
@Configuration
|
|
public final class C {
|
|
int i = 10;
|
|
int j = 11;
|
|
}
|
|
```
|
|
|
|
... and a YAML configuration file that contains:
|
|
|
|
```yaml
|
|
i: 20
|
|
k: 30
|
|
```
|
|
|
|
Now, when you use one of the methods below to call `update` for that configuration type and file,
|
|
the configuration instance that `update` returns will have its `i` variable initialized to `20`
|
|
and its `j` variable will have its default of `11`. After the operation, the configuration file will
|
|
contain:
|
|
|
|
```yaml
|
|
i: 20
|
|
j: 11
|
|
```
|
|
|
|
</details>
|
|
|
|
To exemplify the usage of these three methods we assume for the following sections that you have
|
|
implemented the configuration type below and have access to some regular `java.nio.file.Path`
|
|
object `configurationFile`.
|
|
|
|
```java
|
|
@Configuration
|
|
public final class Config { /* some fields */ }
|
|
```
|
|
|
|
#### Way 1
|
|
|
|
The first way is to create a configuration store and use it directly to save, load, or update
|
|
configurations.
|
|
|
|
```java
|
|
YamlConfigurationProperties properties = YamlConfigurationProperties.newBuilder().build();
|
|
YamlConfigurationStore<Config> store = new YamlConfigurationStore<>(Config.class, properties);
|
|
|
|
Config config1 = store.load(configurationFile);
|
|
store.save(config1, configurationFile);
|
|
Config config2 = store.update(configurationFile);
|
|
```
|
|
|
|
#### Way 2
|
|
|
|
The second way is to use the static methods from the `Configurations` class.
|
|
|
|
```java
|
|
Config config1 = Configurations.loadYamlConfiguration(configurationFile, Config.class);
|
|
Configurations.saveYamlConfiguration(configurationFile, Config.class, config1);
|
|
Config config2 = Configurations.updateYamlConfiguration(configurationFile, Config.class);
|
|
```
|
|
|
|
Each of these methods has two additional overloads: One that takes a properties object and another
|
|
that lets you configure a properties object builder. For example, the overloads for the
|
|
`loadYamlConfiguration` method are:
|
|
|
|
```java
|
|
// overload 1
|
|
YamlConfigurationProperties properties = YamlConfigurationProperties.newBuilder()
|
|
.inputNulls(true)
|
|
.outputNulls(false)
|
|
.build();
|
|
Config c1 = Configurations.loadYamlConfiguration(configurationFile, Config.class, properties);
|
|
|
|
// overload 2
|
|
Config c2 = Configurations.loadYamlConfiguration(
|
|
configurationFile,
|
|
Config.class,
|
|
builder -> builder.inputNulls(true).outputNulls(false)
|
|
);
|
|
```
|
|
|
|
#### Configuration properties
|
|
|
|
Instances of the `ConfigurationProperties` class allow customization of how configurations are
|
|
stored and loaded. To create such an instance, instantiate a new builder using
|
|
the `YamlConfigurationProperties.newBuilder()` method, configure it, and finally call its `build()`
|
|
method. Check out the several methods of the builder to see which configuration options are
|
|
available.
|
|
|
|
#### Ignoring and filtering fields
|
|
|
|
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.
|
|
|
|
#### Handling of `null` values
|
|
|
|
Configuration properties let you configure how `null` values are handled when serializing and
|
|
deserializing a configuration:
|
|
|
|
* By setting `outputNulls` to false, fields and collection elements that are null are not output.
|
|
Any comments that belong to such fields are also not written.
|
|
* By setting `inputNulls` to false, fields and collection elements that are null are not input. That
|
|
means that fields will keep their default values.
|
|
|
|
<details>
|
|
<summary>Example <code>null</code> handling configuration</summary>
|
|
|
|
The following code forbids null values to be output but allows null values to be input. By default,
|
|
both are forbidden which makes the call to `outputNulls` in this case redundant.
|
|
|
|
```java
|
|
YamlConfigurationProperties.newBuilder()
|
|
.outputNulls(false)
|
|
.inputNulls(true)
|
|
.build();
|
|
```
|
|
|
|
</details>
|
|
|
|
#### Field formatting
|
|
|
|
You can define how fields are formatted by configuring the configuration properties with a custom
|
|
formatter. Field formatters are implementations of the `FieldFormatter` interface. You can implement
|
|
this interface yourself or use one of the several formatters this library provides. These
|
|
pre-defined formatters can be found in the `FieldFormatters` class.
|
|
|
|
<details>
|
|
<summary>Example <code>FieldFormatter</code> configuration</summary>
|
|
|
|
The following code formats fields using the `IDENTITY` formatter (which is the default).
|
|
|
|
```java
|
|
YamlConfigurationProperties.newBuilder()
|
|
.setFieldFormatter(FieldFormatters.IDENTITY)
|
|
.build();
|
|
```
|
|
|
|
</details>
|
|
|
|
### Subclassing
|
|
|
|
Subclassing of configurations types is supported. Subclasses of configuration types don't need to be
|
|
annotated with `@Configuration`. When a configuration is written, the fields of parent classes
|
|
are written before the fields of the child in a top to bottom manner. Parent configurations can
|
|
be `abstract`.
|
|
|
|
#### Shadowing of fields
|
|
|
|
Shadowing of fields refers to the situation where a subclass of configuration has a field that has
|
|
the same name as a field in one of its super classes. Shadowing of fields is currently not
|
|
supported. (This restriction might easily be lifted. If you need this feature, please open an issue
|
|
and describe how to handle name clashes.)
|
|
|
|
### 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.
|
|
|
|
<details>
|
|
<summary>Example of <code>@Comment</code> usage </summary>
|
|
|
|
Serializing the following configuration as YAML ...
|
|
|
|
```java
|
|
@Configuration
|
|
public final class ExampleConfiguration {
|
|
@Comment({"Hello", "", " ", "World"})
|
|
private String commentedField = "commented field";
|
|
}
|
|
```
|
|
|
|
... results in the YAML file shown below:
|
|
|
|
```yaml
|
|
# Hello
|
|
|
|
#
|
|
# World
|
|
commentedField: commented field
|
|
```
|
|
|
|
</details>
|
|
|
|
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.
|
|
|
|
### Recursive type definitions
|
|
|
|
Recursive type definitions are currently not allowed but might be supported in a future version if
|
|
this feature is requested.
|
|
|
|
<details>
|
|
<summary>Examples of recursive type definitions</summary>
|
|
|
|
Neither direct nor indirect recursive type definitions are supported.
|
|
|
|
```java
|
|
public final class RecursiveTypDefinitions {
|
|
// Direct recursive definition
|
|
@Configuration
|
|
static final class R {
|
|
R r;
|
|
}
|
|
|
|
// Indirect recursive definition
|
|
@Configuration
|
|
static final class R1 {
|
|
R2 r2;
|
|
}
|
|
|
|
@Configuration
|
|
static final class R2 {
|
|
R1 r1;
|
|
}
|
|
}
|
|
```
|
|
|
|
</details>
|
|
|
|
### Type conversion and custom serializers
|
|
|
|
Before instances of the types listed in the [supported types](#supported-types) section can be
|
|
stored, they need to be converted into serializable types (i.e. into types the underlying YAML
|
|
library knows how to handle). The conversion happens according to the following table:
|
|
|
|
| Source type | Target type |
|
|
|------------------------|------------------|
|
|
| Boolean types | `Boolean` |
|
|
| Integer types | `Long` |
|
|
| Floating point types | `Double` |
|
|
| Characters and strings | `String` |
|
|
| Big numeric types | `String` |
|
|
| Time related types | `String` |
|
|
| Enums | `String` |
|
|
| Configurations | `Map<String, ?>` |
|
|
| `Set<S>` | `List<T>`* |
|
|
| `List<S>` | `List<T>` |
|
|
| `S[]` | `List<T>` |
|
|
| `Map<S1, S2>` | `Map<T1, T2>` |
|
|
|
|
(*) By default, sets are serialized as lists. This can be changed through the configuration
|
|
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
|
|
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.
|
|
|
|
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<A>` 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.
|
|
|
|
<details>
|
|
<summary>Example <code>Serializer</code> implementation</summary>
|
|
|
|
The following `Serializer` serializes instances of `java.awt.Point` into strings.
|
|
|
|
```java
|
|
public final class PointSerializer implements Serializer<Point, String> {
|
|
@Override
|
|
public String serialize(Point element) {
|
|
return element.x + ":" + element.y;
|
|
}
|
|
|
|
@Override
|
|
public Point deserialize(String element) {
|
|
String[] parts = element.split(":");
|
|
int x = Integer.parseInt(parts[0]);
|
|
int y = Integer.parseInt(parts[1]);
|
|
return new Point(x, y);
|
|
}
|
|
}
|
|
```
|
|
|
|
</details>
|
|
|
|
Custom serializers takes precedence over the serializers provided by this library.
|
|
|
|
### Changing the type of fields
|
|
|
|
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.
|
|
|
|
## Import
|
|
|
|
**INFO:** I'm currently looking for an easier way for you to import this library that does not
|
|
require authentication with GitHub. Please check
|
|
this [issue](https://github.com/Exlll/ConfigLib/issues/12) if you have authentication problems.
|
|
|
|
To use this library, import it into your project with either Maven or Gradle as shown in the two
|
|
sections below. This library has additional dependencies (namely, a YAML parser) which are not
|
|
included in the artifact you import.
|
|
|
|
This repository provides plugin versions of this library which bundle all its dependencies, so you
|
|
don't have to worry about them. Also, these versions make it easier for you to update this library
|
|
if you have written multiple plugins that use it.
|
|
|
|
The plugin versions can be downloaded from
|
|
the [releases page](https://github.com/Exlll/ConfigLib/releases) where you can identify them by
|
|
their `-paper-`, `-waterfall-`, and `-velocity-` infix and `-all` suffix. Other than that, the
|
|
plugin versions currently don't add any additional functionality. If you use these versions, don't
|
|
forget to add them as a dependency in the `plugin.yml` (for Paper and Waterfall) or to the
|
|
dependencies array (for Velocity) of your own plugin.
|
|
|
|
Alternatively, if you don't want to use an extra plugin, you can shade the `-core` version and the
|
|
YAML parser yourself.
|
|
|
|
#### Maven
|
|
|
|
```xml
|
|
<repository>
|
|
<id>de.exlll</id>
|
|
<url>https://maven.pkg.github.com/Exlll/ConfigLib</url>
|
|
</repository>
|
|
|
|
<dependency>
|
|
<groupId>de.exlll</groupId>
|
|
<artifactId>configlib-core</artifactId>
|
|
<version>3.0.0</version>
|
|
</dependency>
|
|
```
|
|
|
|
#### Gradle
|
|
|
|
```groovy
|
|
repositories { maven { url 'https://maven.pkg.github.com/Exlll/ConfigLib' } }
|
|
|
|
dependencies { implementation 'de.exlll:configlib-core:3.0.0' }
|
|
```
|
|
|
|
```kotlin
|
|
repositories { maven { url = uri("https://maven.pkg.github.com/Exlll/ConfigLib") } }
|
|
|
|
dependencies { implementation("de.exlll:configlib-core:3.0.0") }
|
|
```
|
|
|
|
## Future work
|
|
|
|
This section contains ideas for upcoming features. If you want any of these to happen any time soon,
|
|
please [open an issue](https://github.com/Exlll/ConfigLib/issues/new) where we can discuss the
|
|
details.
|
|
|
|
- Optional fields
|
|
- Post load / pre save hooks
|
|
- TOML support
|
|
- Change the order of fields in parent/child class scenarios
|
|
- Recursive definitions |