diff --git a/ConfigLib-Core/src/main/java/de/exlll/configlib/annotation/ElementType.java b/ConfigLib-Core/src/main/java/de/exlll/configlib/annotation/ElementType.java
index 96a0e43..9267b20 100644
--- a/ConfigLib-Core/src/main/java/de/exlll/configlib/annotation/ElementType.java
+++ b/ConfigLib-Core/src/main/java/de/exlll/configlib/annotation/ElementType.java
@@ -5,9 +5,11 @@ import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
- * Indicates the of elements a {@code Collection} or {@code Map} contains.
+ * Indicates the type of elements a {@code Collection} or {@code Map} contains.
*
- * This annotation must be used if element type is not simple.
+ * This annotation must only be used if a {@code Collection} or {@code Map} contains
+ * elements whose type is not simple. Note that {@code Map} keys can only be of some
+ * simple type.
*/
@Target(java.lang.annotation.ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
diff --git a/ConfigLib-Core/src/main/java/de/exlll/configlib/annotation/NoConvert.java b/ConfigLib-Core/src/main/java/de/exlll/configlib/annotation/NoConvert.java
index 666a6bb..78322db 100644
--- a/ConfigLib-Core/src/main/java/de/exlll/configlib/annotation/NoConvert.java
+++ b/ConfigLib-Core/src/main/java/de/exlll/configlib/annotation/NoConvert.java
@@ -6,7 +6,7 @@ import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
- * Indicates that the annotated field should not be converted but used as is.
+ * Indicates that the annotated field should not be converted but instead used as is.
*
* This may be useful if the configuration knows how to (de-)serialize
* instances of that type. For example, a {@code BukkitYamlConfiguration}
diff --git a/README.md b/README.md
index 0ad09ba..d11ffbe 100644
--- a/README.md
+++ b/README.md
@@ -9,11 +9,13 @@ stored to files or other storage systems.
Currently this library only supports storing configurations as YAML. However, users may provide their own
storage systems.
+For a step-by-step tutorial see: [Tutorial](https://github.com/Exlll/ConfigLib/wiki/Tutorial)
+
## Features
* automatic creation, saving, loading and updating of configurations
* (_YAML_) automatic creation of files and directories
* support for all primitive types, their wrapper types and `String`s
-* support for `List`s, `Set`s and `Map`s
+* support for (nested) `List`s, `Set`s and `Map`s
* support for `Enum`s and POJOs
* option to add explanatory comments by annotating classes and their fields
* option to provide custom configuration sources
@@ -59,6 +61,9 @@ subclass of `A` and `A` is a subclass of `YamlConfiguration` and you save or loa
fields of class `B` will be saved or loaded, respectively.
## How-to (_YAML_)
+
+For a step-by-step tutorial see: [Tutorial](https://github.com/Exlll/ConfigLib/wiki/Tutorial)
+
#### Creating configurations
To create a YAML configuration, create a new class and extend `YamlConfiguration`. If you write a Bukkit plugin,
you can alternatively extend `BukkitYamlConfiguration` which is a subclass of `YamlConfiguration` and can
@@ -181,7 +186,7 @@ class MyConfiguration extends YamlConfiguration {
```
Note: Even though sets are supported, their YAML-representation is 'pretty ugly', so it's better to use lists instead.
-If you need the set behavior, you can internally use lists and convert them to sets using the `preSave/postLoad`-hooks.
+If you need set behavior, you can internally use lists and convert them to sets using the `preSave/postLoad`-hooks.
Lists, sets and maps that contain other types (e.g. custom types or enums) must use the `ElementType` annotation.
Only simple types can be used as map keys.
@@ -351,6 +356,10 @@ class MyConfiguration extends YamlConfiguration {
Note: Only a single converter instance is created which is cached.
## Example
+
+For a step-by-step tutorial of a more complex example see:
+[Tutorial](https://github.com/Exlll/ConfigLib/wiki/Tutorial)
+
```java
import de.exlll.configlib.annotation.Comment;
import de.exlll.configlib.annotation.ConfigurationElement;
diff --git a/docs/advanced.md b/docs/advanced.md
deleted file mode 100644
index 620986a..0000000
--- a/docs/advanced.md
+++ /dev/null
@@ -1,7 +0,0 @@
-This section covers some the advanced features of this library.
-
-## Converters
-
-
-## Configuration sources
-
diff --git a/docs/custom-config.md b/docs/custom-config.md
new file mode 100644
index 0000000..ae610a4
--- /dev/null
+++ b/docs/custom-config.md
@@ -0,0 +1,181 @@
+This tutorial is intended to show how to add a custom configuration source. So, let's say you
+want to implement your own `Configuration` type, an `InMemoryConfiguration`, for example.
+
+#### 1. Extend `Configuration`
+
+The first thing you have to do is to let your `InMemoryConfiguration` class
+extend `Configuration`.
+
+```java
+import de.exlll.configlib.Configuration;
+import de.exlll.configlib.ConfigurationSource;
+
+public class InMemoryConfiguration extends Configuration {}
+```
+
+#### 2. Create a `ConfigurationSource`
+
+The second step is to create a class that implements
+`ConfigurationSource`.
+
+```java
+final class InMemoryConfigurationSource
+ implements ConfigurationSource {}
+```
+
+#### 3. Implement `save-/loadConfiguration`
+
+Next implement its methods.
+
+```java
+final class InMemoryConfigurationSource
+ implements ConfigurationSource {
+ private Map configAsMap = new HashMap<>();
+
+ /* The 'config' parameter is the Configuration instance that
+ * requested the save/load */
+ @Override
+ public void saveConfiguration(
+ InMemoryConfiguration config, Map map
+ ) {
+ this.configAsMap = map;
+ }
+
+ @Override
+ public Map loadConfiguration(
+ InMemoryConfiguration config
+ ) {
+ return configAsMap;
+ }
+}
+```
+#### 4. Extend `Properties` and `Builder`
+
+Within your `InMemoryConfiguration` class, create an `InMemoryProperties` class
+that extends `Configuration.Properties`. Within the `InMemoryProperties` class,
+create a `Builder` class that extends `Properties.Builder`.
+
+```java
+public class InMemoryConfiguration extends Configuration {
+
+ public static final class InMemoryProperties extends Properties {
+ protected InMemoryProperties(Builder> builder) {
+ super(builder);
+ }
+
+ public static Builder> builder() {
+ return new Builder() {
+ @Override
+ protected Builder getThis() {
+ return this;
+ }
+ };
+ }
+
+ public static abstract class Builder>
+ extends Properties.Builder {
+ public InMemoryProperties build() {
+ return new InMemoryProperties(this);
+ }
+ }
+ }
+}
+```
+
+#### 5. Add properties
+
+Add properties to your `InMemoryProperties` class that can configure your
+`InMemoryConfiguration` in some meaningful way. For example:
+
+```java
+public static final class InMemoryProperties extends Properties {
+ private final int minMemory;
+ private final int maxMemory;
+
+ protected InMemoryProperties(Builder> builder) {
+ super(builder);
+ this.minMemory = builder.minMemory;
+ this.maxMemory = builder.maxMemory;
+ }
+
+ public int getMinMemory() { return minMemory; }
+
+ public int getMaxMemory() { return maxMemory; }
+
+ public static Builder> builder() {
+ return new Builder() {
+ @Override protected Builder getThis() { return this; }
+ };
+ }
+
+ public static abstract class Builder>
+ extends Properties.Builder {
+ private int minMemory = 0;
+ private int maxMemory = 1024;
+
+ public B setMinMemory(int minMemory) {
+ this.minMemory = minMemory;
+ return getThis();
+ }
+
+ public B setMaxMemory(int maxMemory) {
+ this.maxMemory = maxMemory;
+ return getThis();
+ }
+
+ public InMemoryProperties build() {
+ return new InMemoryProperties(this);
+ }
+ }
+}
+```
+
+#### 6. Implement `getSource/getThis`
+
+The last step is to implement the `getSource` and `getThis` methods and override the
+constructor of your `InMemoryConfiguration`.
+
+```java
+public class InMemoryConfiguration extends Configuration {
+ private final InMemoryConfigurationSource source =
+ new InMemoryConfigurationSource();
+
+ protected InMemoryConfiguration(InMemoryProperties properties) {
+ super(properties);
+ }
+
+ @Override
+ protected ConfigurationSource getSource() {
+ return source;
+ }
+
+ @Override
+ protected InMemoryConfiguration getThis() {
+ return this;
+ }
+}
+```
+
+#### 7. Use your new `InMemoryConfiguration`
+
+```java
+final class InMemoryDatabaseConfig extends InMemoryConfiguration {
+ private final String host = "localhost";
+ // ...
+
+ public InMemoryDatabaseConfig(InMemoryProperties properties) {
+ super(properties);
+ }
+ // ...
+
+ public static void main(String[] args) {
+ InMemoryProperties properties = InMemoryProperties.builder()
+ .setMinMemory(123)
+ .setMaxMemory(456)
+ .build();
+
+ InMemoryDatabaseConfig config = new InMemoryDatabaseConfig(properties);
+ config.save();
+ }
+}
+```
\ No newline at end of file
diff --git a/docs/tutorial.md b/docs/tutorial.md
new file mode 100644
index 0000000..e532c83
--- /dev/null
+++ b/docs/tutorial.md
@@ -0,0 +1,521 @@
+## Tutorial
+
+This tutorial is intended to show most of the features of this library, so let's say that
+we want to create the following configuration file for some kind of game:
+
+```yaml
+# Valid color codes: &4, &c, &e
+win_message: '&4YOU WON'
+blocked_users:
+- root
+- john
+team_members:
+- - Pete
+ - Mary
+ - Alice
+ - Leo
+- - Eli
+ - Eve
+ - Paul
+ - Patrick
+moderator:
+ credentials:
+ username: alex
+ password: '123'
+ email: a@b.c
+users_by_name:
+ Patrick:
+ credentials:
+ username: Patrick
+ password: '579'
+ email: patrick@example.com
+ Eli:
+ credentials:
+ username: Eli
+ password: '246'
+ email: eli@example.com
+ Pete:
+ credentials:
+ username: Pete
+ password: '123'
+ email: pete@example.com
+ Eve:
+ credentials:
+ username: Eve
+ password: '357'
+ email: eve@example.com
+ Alice:
+ credentials:
+ username: Alice
+ password: '789'
+ email: alice@example.com
+ Leo:
+ credentials:
+ username: Leo
+ password: '135'
+ email: leo@example.com
+ Paul:
+ credentials:
+ username: Paul
+ password: '468'
+ email: paul@example.com
+ Mary:
+ credentials:
+ username: Mary
+ password: '456'
+ email: mary@example.com
+first_prize:
+ ==: org.bukkit.inventory.ItemStack
+ type: DIAMOND_AXE
+ meta:
+ ==: ItemMeta
+ meta-type: UNSPECIFIC
+ enchants:
+ DIG_SPEED: 5
+ DURABILITY: 3
+ MENDING: 1
+consolation_prizes:
+- ==: org.bukkit.inventory.ItemStack
+ type: STICK
+ amount: 2
+- ==: org.bukkit.inventory.ItemStack
+ type: ROTTEN_FLESH
+ amount: 3
+- ==: org.bukkit.inventory.ItemStack
+ type: CARROT
+ amount: 4
+prohibited_items:
+- BEDROCK
+- AIR
+- LAVA
+
+# Configure the arena:
+arena_height: 40
+arena_center: world;0;128;0
+
+# Remember to play fair!
+```
+
+#### 1. Extend `BukkitYamlConfiguration`
+
+The first thing we have to do is to extend `BukkitYamlConfiguration`. We use
+`BukkitYamlConfiguration` instead of `YamlConfiguration` because it can properly
+(de-)serialize Bukkit classes like `ItemStack`s.
+
+```java
+public final class GameConfig extends BukkitYamlConfiguration {}
+```
+
+#### 2. Create constructor
+
+Next we have to create a constructor that matches super. If we don't pass a
+`BukkitYamlProperties` object to the super call, the `BukkitYamlProperties.DEFAULT`
+instance is used. But because we want to format the field names of our configuration
+(so that we can write 'winMessage' in Java which becomes 'win_message' in YAML), we
+have to pass a `BukkitYamlProperties` object that uses a different
+`FieldNameFormatter` (see [11.](https://github.com/Exlll/ConfigLib/wiki/Tutorial#11-use-gameconfig)).
+
+```java
+public final class GameConfig extends BukkitYamlConfiguration {
+ public GameConfig(Path path, BukkitYamlProperties properties) {
+ super(path, properties);
+ }
+
+ // uses BukkitYamlProperties.DEFAULT instance
+ // public GameConfig(Path path) {
+ // super(path);
+ // }
+}
+```
+
+#### 3. Add `winMessage`
+
+Because `winMessage` is a string with a comment, we add a field named `winMessage`
+of type `String` to our configuration class and annotate it with the `@Comment`
+annotation.
+
+```java
+public final class GameConfig extends BukkitYamlConfiguration {
+ @Comment("Valid color codes: &4, &c, &e")
+ private String winMessage = "&4YOU WON";
+ // ...
+}
+```
+
+#### 4. Add `blockedUsers`
+
+Because `blockedUsers` is a list of strings, we add a field named `blockedUsers`
+of type `List` to our configuration class.
+
+```java
+public final class GameConfig extends BukkitYamlConfiguration {
+ // ...
+ private List blockedUsers = Arrays.asList("root", "john");
+ // ...
+}
+```
+
+Remember that `null` values are not allowed. All non-primitive fields must be
+assigned some non-`null` default value.
+
+#### 5. Add `teamMembers`
+
+The `teamMembers` field is of type `List>`.
+
+```java
+public final class GameConfig extends BukkitYamlConfiguration {
+ // ...
+ private List> teamMembers = Arrays.asList(
+ Arrays.asList("Pete", "Mary", "Alice", "Leo"),
+ Arrays.asList("Eli", "Eve", "Paul", "Patrick")
+ );
+ // ...
+}
+```
+
+#### 6. Add `moderator`
+
+Because our `moderator` represents a user that has credentials and an email address,
+we create a `User` and a `Credentials` class and annotate them as
+`ConfigurationElement`s:
+
+```java
+@ConfigurationElement
+final class Credentials {
+ private String username;
+ private String password;
+
+ // ConfigurationElements must have a no-args constructor (can be private)
+ private Credentials() {}
+
+ public Credentials(String username, String password) {
+ this.username = username;
+ this.password = password;
+ }
+ // getter etc.
+ }
+
+@ConfigurationElement
+final class User {
+ private Credentials credentials;
+ private String email;
+
+ private User() {}
+
+ public User(String username, String password, String email) {
+ this.credentials = new Credentials(username, password);
+ this.email = email;
+ }
+ // getter etc.
+}
+```
+
+Now we can use the `User` class for our `moderator` field:
+
+```java
+public final class GameConfig extends BukkitYamlConfiguration {
+ // ...
+ private User moderator = new User("alex", "a@b.c", "123");
+ // ...
+}
+```
+
+`ConfigurationElement`s must have a no-args constructor which is to create
+instances of a given element. If the no-args constructor is a valid constructor
+for your program, it must initialize the fields to some non-`null` value.
+
+#### 7. Add `usersByName`
+
+The `usersByName` field is a map that maps user names to `User` instances.
+That means we have to use the `@ElementType` annotation.
+
+```java
+public final class GameConfig extends BukkitYamlConfiguration {
+ // ...
+ @ElementType(User.class)
+ private Map usersByName = initUsersByName();
+ // ...
+
+ private Map initUsersByName() {
+ Map usersByName = new HashMap<>();
+ usersByName.put("Pete", new User("Pete", "pete@example.com", "123"));
+ usersByName.put("Mary", new User("Mary", "mary@example.com", "456"));
+ // ...
+ return usersByName;
+ }
+}
+```
+
+#### 8. Add `firstPrize` and `consolationPrizes`
+
+The types of `firstPrize` and `consolationPrizes` are `ItemStack` and `List`,
+respectively. Because a `BukkitYamlConfiguration` knows how to serialize `ItemStack`
+instances, we need to tell the library not to try to convert them. This can be done
+by using the `@NoConvert` annotation.
+
+```java
+public final class GameConfig extends BukkitYamlConfiguration {
+ // ...
+ @NoConvert
+ private ItemStack firstPrize = initFirstPrize();
+ @NoConvert
+ private List consolationPrizes = Arrays.asList(
+ new ItemStack(Material.STICK, 2),
+ new ItemStack(Material.ROTTEN_FLESH, 3),
+ new ItemStack(Material.CARROT, 4)
+ );
+ // ...
+
+ private ItemStack initFirstPrize() {
+ ItemStack stack = new ItemStack(Material.DIAMOND_AXE);
+ stack.addEnchantment(Enchantment.DURABILITY, 3);
+ stack.addEnchantment(Enchantment.DIG_SPEED, 5);
+ stack.addEnchantment(Enchantment.MENDING, 1);
+ return stack;
+ }
+ // ...
+}
+```
+
+#### 9. Add `prohibitedItems`
+
+The `prohibitedItems` field is a list of `Material`s. Since this library supports
+converting enums, we just have to use the `@ElementType` annotation.
+
+```java
+public final class GameConfig extends BukkitYamlConfiguration {
+ // ...
+ @ElementType(Material.class)
+ private List prohibitedItems = Arrays.asList(
+ Material.BEDROCK, Material.AIR, Material.LAVA
+ );
+ // ...
+}
+```
+
+#### 10. Add `arenaHeight` and `arenaCenter`
+
+The `arenaHeight` can simply be represented by an `int` field. The `arenaCenter`
+is of type `Location`. We could again use the `@NoConvert` annotation but this
+would result in a different representation. Instead, we are going to implement our
+`Converter`.
+
+First we have to create a class that implements `Converter`:
+
+```java
+final class LocationStringConverter implements Converter {}
+```
+
+Then we must implement the `convertTo` and `convertFrom` methods:
+
+```java
+final class LocationStringConverter implements Converter {
+
+ @Override
+ public String convertTo(Location location, ConversionInfo conversionInfo) {
+ String worldName = location.getWorld().getName();
+ int blockX = location.getBlockX();
+ int blockY = location.getBlockY();
+ int blockZ = location.getBlockZ();
+ return worldName + ";" + blockX + ";" + blockY + ";" + blockZ;
+ }
+
+ @Override
+ public Location convertFrom(String s, ConversionInfo conversionInfo) {
+ String[] split = s.split(";");
+ World world = Bukkit.getWorld(split[0]);
+ int x = Integer.parseInt(split[1]);
+ int y = Integer.parseInt(split[2]);
+ int z = Integer.parseInt(split[3]);
+ return new Location(world, x, y, z);
+ }
+}
+```
+
+Finally we have to tell our configuration to use this converter for the
+`arenaCenter` field. This is done using the `@Convert` annotation:
+
+```java
+public final class GameConfig extends BukkitYamlConfiguration {
+ // ...
+ @Comment({"", "Configure the arena:"})
+ private int arenaHeight = 40;
+ @Convert(LocationStringConverter.class)
+ private Location arenaCenter = new Location(
+ Bukkit.getWorld("world"), 0, 128, 0
+ );
+}
+```
+
+#### 11. Use `GameConfig`
+
+Before we can use our new configuration, we have to instantiate it by passing
+a `Path` and a `BukkitYamlProperties` object to its constructor. In this case
+the `BukkitYamlProperties` is used to change the formatting and to append
+additional text to the created configuration file.
+
+```java
+public final class GamePlugin extends JavaPlugin {
+
+ @Override
+ public void onEnable() {
+ Path configPath = new File(getDataFolder(), "config.yml").toPath();
+
+ BukkitYamlProperties properties = BukkitYamlProperties.builder()
+ .setFormatter(FieldNameFormatters.LOWER_UNDERSCORE)
+ .setAppendedComments(Arrays.asList(
+ "", "Remember to play fair!"
+ ))
+ .build();
+
+ GameConfig config = new GameConfig(configPath, properties);
+ config.loadAndSave();
+ }
+}
+```
+
+### Full example
+
+```java
+import de.exlll.configlib.Converter;
+import de.exlll.configlib.annotation.*;
+import de.exlll.configlib.configs.yaml.BukkitYamlConfiguration;
+import de.exlll.configlib.configs.yaml.BukkitYamlConfiguration.BukkitYamlProperties;
+import de.exlll.configlib.format.FieldNameFormatters;
+import org.bukkit.Bukkit;
+import org.bukkit.Location;
+import org.bukkit.Material;
+import org.bukkit.World;
+import org.bukkit.enchantments.Enchantment;
+import org.bukkit.inventory.ItemStack;
+import org.bukkit.plugin.java.JavaPlugin;
+
+import java.io.File;
+import java.nio.file.Path;
+import java.util.Arrays;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+public final class GamePlugin extends JavaPlugin {
+ @Override
+ public void onEnable() {
+ Path configPath = new File(getDataFolder(), "config.yml").toPath();
+
+ BukkitYamlProperties properties = BukkitYamlProperties.builder()
+ .setFormatter(FieldNameFormatters.LOWER_UNDERSCORE)
+ .setAppendedComments(Arrays.asList(
+ "", "Remember to play fair!"
+ ))
+ .build();
+ GameConfig config = new GameConfig(configPath, properties);
+ config.loadAndSave();
+ }
+}
+
+final class GameConfig extends BukkitYamlConfiguration {
+ @Comment("Valid color codes: &4, &c, &e")
+ private String winMessage = "&4YOU WON";
+ private User moderator = new User("alex", "a@b.c", "123");
+ private List blockedUsers = Arrays.asList("root", "john");
+ private List> teamMembers = Arrays.asList(
+ Arrays.asList("Pete", "Mary", "Alice", "Leo"),
+ Arrays.asList("Eli", "Eve", "Paul", "Patrick")
+ );
+ @ElementType(User.class)
+ private Map usersByName = initUsersByName();
+ @NoConvert
+ private ItemStack firstPrize = initFirstPrize();
+ @NoConvert
+ private List consolationPrizes = Arrays.asList(
+ new ItemStack(Material.STICK, 2),
+ new ItemStack(Material.ROTTEN_FLESH, 3),
+ new ItemStack(Material.CARROT, 4)
+ );
+ @ElementType(Material.class)
+ private List prohibitedItems = Arrays.asList(
+ Material.BEDROCK, Material.AIR, Material.LAVA
+ );
+ @Comment({"", "Configure the arena:"})
+ private int arenaHeight = 40;
+ @Convert(LocationStringConverter.class)
+ private Location arenaCenter = new Location(
+ Bukkit.getWorld("world"), 0, 128, 0
+ );
+
+ public GameConfig(Path path, BukkitYamlProperties properties) {
+ super(path, properties);
+ }
+
+ private HashMap initUsersByName() {
+ HashMap usersByName = new HashMap<>();
+ usersByName.put("Pete", new User("Pete", "pete@example.com", "123"));
+ usersByName.put("Mary", new User("Mary", "mary@example.com", "456"));
+ usersByName.put("Alice", new User("Alice", "alice@example.com", "789"));
+ usersByName.put("Leo", new User("Leo", "leo@example.com", "135"));
+ usersByName.put("Eli", new User("Eli", "eli@example.com", "246"));
+ usersByName.put("Eve", new User("Eve", "eve@example.com", "357"));
+ usersByName.put("Paul", new User("Paul", "paul@example.com", "468"));
+ usersByName.put("Patrick", new User("Patrick", "patrick@example.com", "579"));
+ return usersByName;
+ }
+
+ private ItemStack initFirstPrize() {
+ ItemStack stack = new ItemStack(Material.DIAMOND_AXE);
+ stack.addEnchantment(Enchantment.DURABILITY, 3);
+ stack.addEnchantment(Enchantment.DIG_SPEED, 5);
+ stack.addEnchantment(Enchantment.MENDING, 1);
+ return stack;
+ }
+
+ private static final class LocationStringConverter
+ implements Converter {
+
+ @Override
+ public String convertTo(Location location, ConversionInfo conversionInfo) {
+ String worldName = location.getWorld().getName();
+ int blockX = location.getBlockX();
+ int blockY = location.getBlockY();
+ int blockZ = location.getBlockZ();
+ return worldName + ";" + blockX + ";" + blockY + ";" + blockZ;
+ }
+
+ @Override
+ public Location convertFrom(String s, ConversionInfo conversionInfo) {
+ String[] split = s.split(";");
+ World world = Bukkit.getWorld(split[0]);
+ int x = Integer.parseInt(split[1]);
+ int y = Integer.parseInt(split[2]);
+ int z = Integer.parseInt(split[3]);
+ return new Location(world, x, y, z);
+ }
+ }
+}
+
+@ConfigurationElement
+final class User {
+ private Credentials credentials;
+ private String email;
+
+ private User() {}
+
+ public User(String username, String password, String email) {
+ this.credentials = new Credentials(username, password);
+ this.email = email;
+ }
+}
+
+@ConfigurationElement
+final class Credentials {
+ private String username;
+ private String password;
+
+ // ConfigurationElements must have a no-args constructor (can be private)
+ private Credentials() {}
+
+ public Credentials(String username, String password) {
+ this.username = username;
+ this.password = password;
+ }
+}
+```
\ No newline at end of file