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