diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..e28e780
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,166 @@
+
+# Created by https://www.toptal.com/developers/gitignore/api/intellij+all,macos,java,gradle
+# Edit at https://www.toptal.com/developers/gitignore?templates=intellij+all,macos,java,gradle
+
+### Intellij+all ###
+# Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio, WebStorm and Rider
+# Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839
+
+# User-specific stuff
+.idea/**/workspace.xml
+.idea/**/tasks.xml
+.idea/**/usage.statistics.xml
+.idea/**/dictionaries
+.idea/**/shelf
+
+# Generated files
+.idea/**/contentModel.xml
+
+# Sensitive or high-churn files
+.idea/**/dataSources/
+.idea/**/dataSources.ids
+.idea/**/dataSources.local.xml
+.idea/**/sqlDataSources.xml
+.idea/**/dynamic.xml
+.idea/**/uiDesigner.xml
+.idea/**/dbnavigator.xml
+
+# Gradle
+.idea/**/gradle.xml
+.idea/**/libraries
+
+# Gradle and Maven with auto-import
+# When using Gradle or Maven with auto-import, you should exclude module files,
+# since they will be recreated, and may cause churn. Uncomment if using
+# auto-import.
+# .idea/artifacts
+# .idea/compiler.xml
+# .idea/jarRepositories.xml
+# .idea/modules.xml
+# .idea/*.iml
+# .idea/modules
+# *.iml
+# *.ipr
+
+# CMake
+cmake-build-*/
+
+# Mongo Explorer plugin
+.idea/**/mongoSettings.xml
+
+# File-based project format
+*.iws
+
+# IntelliJ
+out/
+
+# mpeltonen/sbt-idea plugin
+.idea_modules/
+
+# JIRA plugin
+atlassian-ide-plugin.xml
+
+# Cursive Clojure plugin
+.idea/replstate.xml
+
+# Crashlytics plugin (for Android Studio and IntelliJ)
+com_crashlytics_export_strings.xml
+crashlytics.properties
+crashlytics-build.properties
+fabric.properties
+
+# Editor-based Rest Client
+.idea/httpRequests
+
+# Android studio 3.1+ serialized cache file
+.idea/caches/build_file_checksums.ser
+
+### Intellij+all Patch ###
+# Ignores the whole .idea folder and all .iml files
+# See https://github.com/joeblau/gitignore.io/issues/186 and https://github.com/joeblau/gitignore.io/issues/360
+
+.idea/
+
+# Reason: https://github.com/joeblau/gitignore.io/issues/186#issuecomment-249601023
+
+*.iml
+modules.xml
+.idea/misc.xml
+*.ipr
+
+# Sonarlint plugin
+.idea/sonarlint
+
+### Java ###
+# Compiled class file
+*.class
+
+# Log file
+*.log
+
+# BlueJ files
+*.ctxt
+
+# Mobile Tools for Java (J2ME)
+.mtj.tmp/
+
+# Package Files #
+*.jar
+*.war
+*.nar
+*.ear
+*.zip
+*.tar.gz
+*.rar
+
+# virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml
+hs_err_pid*
+
+### macOS ###
+# General
+.DS_Store
+.AppleDouble
+.LSOverride
+
+# Icon must end with two \r
+Icon
+
+# Thumbnails
+._*
+
+# Files that might appear in the root of a volume
+.DocumentRevisions-V100
+.fseventsd
+.Spotlight-V100
+.TemporaryItems
+.Trashes
+.VolumeIcon.icns
+.com.apple.timemachine.donotpresent
+
+# Directories potentially created on remote AFP share
+.AppleDB
+.AppleDesktop
+Network Trash Folder
+Temporary Items
+.apdisk
+
+### Gradle ###
+.gradle
+build/
+
+# Ignore Gradle GUI config
+gradle-app.setting
+
+# Avoid ignoring Gradle wrapper jar file (.jar files are usually ignored)
+!gradle-wrapper.jar
+
+# Cache of project
+.gradletasknamecache
+
+# # Work around https://youtrack.jetbrains.com/issue/IDEA-116898
+# gradle/wrapper/gradle-wrapper.properties
+
+### Gradle Patch ###
+**/build/
+
+# End of https://www.toptal.com/developers/gitignore/api/intellij+all,macos,java,gradle
\ No newline at end of file
diff --git a/README.MD b/README.MD
new file mode 100644
index 0000000..5200176
--- /dev/null
+++ b/README.MD
@@ -0,0 +1,193 @@
+# ConfigAPI
+Config API for Bukkit 1.8 - 1.16 based on Dynamic Proxies
+
+## Features:
+ - Create config via Interface with `default` getters and setters
+ - Automatic generation of YAML files based on Java code
+ - Automatic updates of YAML file after adding new field to config
+ - Support of comments in YAML files
+ - Support of multiple config files
+ - System of serializers for custom objects (e.g. ItemStack, Location)
+ - Automatic translation of `&` based colors
+
+## Import
+#### Gradle
+```groovy
+maven {
+ url = 'https://repo.mikigal.pl/releases'
+}
+
+compile group: 'pl.mikigal', name: 'ConfigAPI', version: '1.0'
+```
+
+#### Maven
+```xml
+
+ mikigal-repo
+ https://repo.mikigal.pl/releases
+
+
+
+ pl.mikigal
+ ConfigAPI
+ 1.0
+ compile
+
+```
+
+## How to use?
+#### Java code
+```java
+public class TestPlugin extends JavaPlugin {
+
+ private static TestConfig testConfig;
+
+ @Override
+ public void onEnable() {
+ testConfig = ConfigAPI.init(
+ TestConfig.class, // Class of config's interface
+ NameStyle.UNDERSCORE, // Style of fields' name in YAML file
+ CommentStyle.INLINE, // Style of comments in YAML file
+ true, // Automatic translation of '&' based colors
+ this // Instance of plugin
+ );
+
+ // You can simply access data from the config by getters
+ System.out.println(testConfig.getExampleMessage());
+ Bukkit.getPlayer("mikigal").getInventory().addItem(testConfig.getAward());
+
+ // After calling setter new data are automatically saved to file
+ testConfig.setAward(new ItemStack(Material.DIRT));
+
+ // If you want to do something manually you can access instance of YamlConfiguration
+ testConfig.getBukkitConfiguration();
+ }
+
+ public static TestConfig getTestConfig() {
+ return testConfig;
+ }
+}
+
+@ConfigName("test.yml") // Name of YAML file
+public interface TestConfig extends Config {
+
+ @Comment("This comment will be saved to YAML file!")
+ default String getExampleMessage() {
+ // Getter method should return default value of the field
+ return "&cIt's default value of example message";
+ }
+
+ default ItemStack getAward() {
+ return Item.of(Material.DIAMOND_SWORD)
+ .name("&cAward")
+ .lore("&aFirst line", "&cSecond line")
+ .toItem();
+ }
+
+ public void setAward(ItemStack award);
+
+ // Key of Map must be String
+ default Map getValues() {
+ Map map = new HashMap<>();
+ map.put("a", 1);
+ map.put("b", 2);
+
+ return map;
+ }
+
+ default List getSpawnPoints() {
+ World world = Bukkit.getWorld("world");
+ return Arrays.asList(
+ new Location(world, 0, 100, 0),
+ new Location(world, 10, 90, 10, 90f, 0f),
+ new Location(world, 20, 80, 20, 0f, 180f)
+ );
+ }
+}
+```
+
+#### Automatic generated YAML from above Java code
+```yaml
+values:
+ a: 1
+ b: 2
+example_message: '&cIt''s default value of example message' # This comment will be saved to YAML file!
+spawn_points:
+ type: org.bukkit.Location
+ '0':
+ world: world
+ x: 0.0
+ y: 100.0
+ z: 0.0
+ yaw: 0.0
+ pitch: 0.0
+ '1':
+ world: world
+ x: 10.0
+ y: 90.0
+ z: 10.0
+ yaw: 90.0
+ pitch: 0.0
+ '2':
+ world: world
+ x: 20.0
+ y: 80.0
+ z: 20.0
+ yaw: 0.0
+ pitch: 180.0
+award:
+ material: DIAMOND_SWORD
+ amount: 1
+ name: '&cAward'
+ lore:
+ - '&aFirst line'
+ - '&cSecond line'
+
+```
+
+## Serializers
+### API has built-in serializers for:
+ - ItemStack
+ - Location
+ - PotionEffect
+ - ShapedRecipe
+ - UUID
+
+#### You can also make your own serializers
+```java
+public class PotionEffectSerializer extends Serializer {
+
+ @Override
+ protected void saveObject(String path, PotionEffect object, BukkitConfiguration configuration) {
+ // In saveObject() method you have to set data of object to config. You can use set() method to set another object which need serialization too
+ configuration.set(path + ".type", object.getType().getName());
+ configuration.set(path + ".duration", object.getDuration());
+ configuration.set(path + ".amplifier", object.getAmplifier());
+ }
+
+ @Override
+ public PotionEffect deserialize(String path, BukkitConfiguration configuration) {
+ // In deserialize() method you have to load data from config and return instance of object
+ PotionEffectType type = PotionEffectType.getByName(configuration.getString(path + ".type"));
+ int duration = configuration.getInt(path + ".duration");
+ int amplifier = configuration.getInt(path + ".amplifier");
+
+ if (type == null) {
+ throw new InvalidConfigFileException("Invalid PotionEffect type (path: " + path + ")");
+ }
+
+ return new PotionEffect(type, duration, amplifier);
+ }
+}
+
+public class TestPlugin extends JavaPlugin {
+
+ @Override
+ public void onEnable() {
+ // Remember to register you Serializer before use!
+ ConfigAPI.registerSerializer(PotionEffect.class, new PotionEffectSerializer());
+
+ // Init your configs...
+
+ }
+}
\ No newline at end of file
diff --git a/build.gradle b/build.gradle
new file mode 100644
index 0000000..93393f2
--- /dev/null
+++ b/build.gradle
@@ -0,0 +1,47 @@
+plugins {
+ id 'java'
+ id 'maven-publish'
+}
+
+group 'pl.mikigal'
+version '1.0'
+
+publishing {
+ repositories {
+ maven {
+ name = "repo.mikigal.pl"
+ url = uri("https://repo.mikigal.pl/releases")
+ credentials {
+ username = System.getenv("REPO_USERNAME")
+ password = System.getenv("REPO_TOKEN")
+ }
+ }
+ }
+ publications {
+ distribution(MavenPublication) {
+ from(components.java)
+ }
+ }
+}
+
+repositories {
+ mavenCentral()
+
+ maven {
+ url = 'https://hub.spigotmc.org/nexus/content/repositories/snapshots/'
+ }
+ maven {
+ url = "https://oss.sonatype.org/content/repositories/snapshots/"
+ }
+}
+
+dependencies {
+ testCompile group: 'junit', name: 'junit', version: '4.12'
+ compileOnly group: 'org.spigotmc', name: 'spigot-api', version: '1.8.8-R0.1-SNAPSHOT'
+}
+
+jar {
+ from {
+ configurations.compile.collect { it.isDirectory() ? it : zipTree(it) }
+ }
+}
\ No newline at end of file
diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar
new file mode 100644
index 0000000..f3d88b1
Binary files /dev/null and b/gradle/wrapper/gradle-wrapper.jar differ
diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties
new file mode 100644
index 0000000..ba94df8
--- /dev/null
+++ b/gradle/wrapper/gradle-wrapper.properties
@@ -0,0 +1,5 @@
+distributionBase=GRADLE_USER_HOME
+distributionPath=wrapper/dists
+distributionUrl=https\://services.gradle.org/distributions/gradle-6.1-bin.zip
+zipStoreBase=GRADLE_USER_HOME
+zipStorePath=wrapper/dists
diff --git a/gradlew b/gradlew
new file mode 100755
index 0000000..2fe81a7
--- /dev/null
+++ b/gradlew
@@ -0,0 +1,183 @@
+#!/usr/bin/env sh
+
+#
+# Copyright 2015 the original author or authors.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# https://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+##############################################################################
+##
+## Gradle start up script for UN*X
+##
+##############################################################################
+
+# Attempt to set APP_HOME
+# Resolve links: $0 may be a link
+PRG="$0"
+# Need this for relative symlinks.
+while [ -h "$PRG" ] ; do
+ ls=`ls -ld "$PRG"`
+ link=`expr "$ls" : '.*-> \(.*\)$'`
+ if expr "$link" : '/.*' > /dev/null; then
+ PRG="$link"
+ else
+ PRG=`dirname "$PRG"`"/$link"
+ fi
+done
+SAVED="`pwd`"
+cd "`dirname \"$PRG\"`/" >/dev/null
+APP_HOME="`pwd -P`"
+cd "$SAVED" >/dev/null
+
+APP_NAME="Gradle"
+APP_BASE_NAME=`basename "$0"`
+
+# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
+DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
+
+# Use the maximum available, or set MAX_FD != -1 to use that value.
+MAX_FD="maximum"
+
+warn () {
+ echo "$*"
+}
+
+die () {
+ echo
+ echo "$*"
+ echo
+ exit 1
+}
+
+# OS specific support (must be 'true' or 'false').
+cygwin=false
+msys=false
+darwin=false
+nonstop=false
+case "`uname`" in
+ CYGWIN* )
+ cygwin=true
+ ;;
+ Darwin* )
+ darwin=true
+ ;;
+ MINGW* )
+ msys=true
+ ;;
+ NONSTOP* )
+ nonstop=true
+ ;;
+esac
+
+CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
+
+# Determine the Java command to use to start the JVM.
+if [ -n "$JAVA_HOME" ] ; then
+ if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
+ # IBM's JDK on AIX uses strange locations for the executables
+ JAVACMD="$JAVA_HOME/jre/sh/java"
+ else
+ JAVACMD="$JAVA_HOME/bin/java"
+ fi
+ if [ ! -x "$JAVACMD" ] ; then
+ die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
+
+Please set the JAVA_HOME variable in your environment to match the
+location of your Java installation."
+ fi
+else
+ JAVACMD="java"
+ which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
+
+Please set the JAVA_HOME variable in your environment to match the
+location of your Java installation."
+fi
+
+# Increase the maximum file descriptors if we can.
+if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then
+ MAX_FD_LIMIT=`ulimit -H -n`
+ if [ $? -eq 0 ] ; then
+ if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then
+ MAX_FD="$MAX_FD_LIMIT"
+ fi
+ ulimit -n $MAX_FD
+ if [ $? -ne 0 ] ; then
+ warn "Could not set maximum file descriptor limit: $MAX_FD"
+ fi
+ else
+ warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT"
+ fi
+fi
+
+# For Darwin, add options to specify how the application appears in the dock
+if $darwin; then
+ GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\""
+fi
+
+# For Cygwin or MSYS, switch paths to Windows format before running java
+if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then
+ APP_HOME=`cygpath --path --mixed "$APP_HOME"`
+ CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
+ JAVACMD=`cygpath --unix "$JAVACMD"`
+
+ # We build the pattern for arguments to be converted via cygpath
+ ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null`
+ SEP=""
+ for dir in $ROOTDIRSRAW ; do
+ ROOTDIRS="$ROOTDIRS$SEP$dir"
+ SEP="|"
+ done
+ OURCYGPATTERN="(^($ROOTDIRS))"
+ # Add a user-defined pattern to the cygpath arguments
+ if [ "$GRADLE_CYGPATTERN" != "" ] ; then
+ OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)"
+ fi
+ # Now convert the arguments - kludge to limit ourselves to /bin/sh
+ i=0
+ for arg in "$@" ; do
+ CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -`
+ CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option
+
+ if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition
+ eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"`
+ else
+ eval `echo args$i`="\"$arg\""
+ fi
+ i=`expr $i + 1`
+ done
+ case $i in
+ 0) set -- ;;
+ 1) set -- "$args0" ;;
+ 2) set -- "$args0" "$args1" ;;
+ 3) set -- "$args0" "$args1" "$args2" ;;
+ 4) set -- "$args0" "$args1" "$args2" "$args3" ;;
+ 5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;;
+ 6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;;
+ 7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;;
+ 8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;;
+ 9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;;
+ esac
+fi
+
+# Escape application args
+save () {
+ for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done
+ echo " "
+}
+APP_ARGS=`save "$@"`
+
+# Collect all arguments for the java command, following the shell quoting and substitution rules
+eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS"
+
+exec "$JAVACMD" "$@"
diff --git a/gradlew.bat b/gradlew.bat
new file mode 100644
index 0000000..9618d8d
--- /dev/null
+++ b/gradlew.bat
@@ -0,0 +1,100 @@
+@rem
+@rem Copyright 2015 the original author or authors.
+@rem
+@rem Licensed under the Apache License, Version 2.0 (the "License");
+@rem you may not use this file except in compliance with the License.
+@rem You may obtain a copy of the License at
+@rem
+@rem https://www.apache.org/licenses/LICENSE-2.0
+@rem
+@rem Unless required by applicable law or agreed to in writing, software
+@rem distributed under the License is distributed on an "AS IS" BASIS,
+@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+@rem See the License for the specific language governing permissions and
+@rem limitations under the License.
+@rem
+
+@if "%DEBUG%" == "" @echo off
+@rem ##########################################################################
+@rem
+@rem Gradle startup script for Windows
+@rem
+@rem ##########################################################################
+
+@rem Set local scope for the variables with windows NT shell
+if "%OS%"=="Windows_NT" setlocal
+
+set DIRNAME=%~dp0
+if "%DIRNAME%" == "" set DIRNAME=.
+set APP_BASE_NAME=%~n0
+set APP_HOME=%DIRNAME%
+
+@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
+set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
+
+@rem Find java.exe
+if defined JAVA_HOME goto findJavaFromJavaHome
+
+set JAVA_EXE=java.exe
+%JAVA_EXE% -version >NUL 2>&1
+if "%ERRORLEVEL%" == "0" goto init
+
+echo.
+echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
+echo.
+echo Please set the JAVA_HOME variable in your environment to match the
+echo location of your Java installation.
+
+goto fail
+
+:findJavaFromJavaHome
+set JAVA_HOME=%JAVA_HOME:"=%
+set JAVA_EXE=%JAVA_HOME%/bin/java.exe
+
+if exist "%JAVA_EXE%" goto init
+
+echo.
+echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
+echo.
+echo Please set the JAVA_HOME variable in your environment to match the
+echo location of your Java installation.
+
+goto fail
+
+:init
+@rem Get command-line arguments, handling Windows variants
+
+if not "%OS%" == "Windows_NT" goto win9xME_args
+
+:win9xME_args
+@rem Slurp the command line arguments.
+set CMD_LINE_ARGS=
+set _SKIP=2
+
+:win9xME_args_slurp
+if "x%~1" == "x" goto execute
+
+set CMD_LINE_ARGS=%*
+
+:execute
+@rem Setup the command line
+
+set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
+
+@rem Execute Gradle
+"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS%
+
+:end
+@rem End local scope for the variables with windows NT shell
+if "%ERRORLEVEL%"=="0" goto mainEnd
+
+:fail
+rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
+rem the _cmd.exe /c_ return code!
+if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
+exit /b 1
+
+:mainEnd
+if "%OS%"=="Windows_NT" endlocal
+
+:omega
diff --git a/settings.gradle b/settings.gradle
new file mode 100644
index 0000000..acb4b09
--- /dev/null
+++ b/settings.gradle
@@ -0,0 +1,2 @@
+rootProject.name = 'ConfigAPI'
+
diff --git a/src/main/java/pl/mikigal/config/BukkitConfiguration.java b/src/main/java/pl/mikigal/config/BukkitConfiguration.java
new file mode 100644
index 0000000..6acbb20
--- /dev/null
+++ b/src/main/java/pl/mikigal/config/BukkitConfiguration.java
@@ -0,0 +1,231 @@
+package pl.mikigal.config;
+
+import org.bukkit.configuration.InvalidConfigurationException;
+import org.bukkit.configuration.file.YamlConfiguration;
+import org.bukkit.configuration.file.YamlRepresenter;
+import org.yaml.snakeyaml.DumperOptions;
+import org.yaml.snakeyaml.Yaml;
+import pl.mikigal.config.annotation.Comment;
+import pl.mikigal.config.exception.InvalidConfigException;
+import pl.mikigal.config.exception.MissingSerializerException;
+import pl.mikigal.config.serializer.Serializer;
+import pl.mikigal.config.serializer.Serializers;
+import pl.mikigal.config.style.CommentStyle;
+import pl.mikigal.config.style.NameStyle;
+import pl.mikigal.config.util.ConversionUtils;
+import pl.mikigal.config.util.TypeUtils;
+
+import java.io.*;
+import java.lang.reflect.Field;
+import java.util.*;
+
+public class BukkitConfiguration extends YamlConfiguration {
+
+ private static final Field yamlOptionsField;
+ private static final Field yamlRepresenterField;
+ private static final Field yamlField;
+
+ static {
+ try {
+ yamlOptionsField = YamlConfiguration.class.getDeclaredField("yamlOptions");
+ yamlOptionsField.setAccessible(true);
+
+ yamlRepresenterField = YamlConfiguration.class.getDeclaredField("yamlRepresenter");
+ yamlRepresenterField.setAccessible(true);
+
+ yamlField = YamlConfiguration.class.getDeclaredField("yaml");
+ yamlField.setAccessible(true);
+ } catch (NoSuchFieldException e) {
+ throw new InvalidConfigException("Could not find Fields of YamlConfiguration", e);
+ }
+ }
+
+ private final File file;
+ private final NameStyle nameStyle;
+ private final CommentStyle commentStyle;
+ private final boolean automaticColorStrings;
+ private final Map cache;
+ private final Map comments;
+
+ private final DumperOptions yamlOptions;
+ private final YamlRepresenter yamlRepresenter;
+ private final Yaml yaml;
+
+ public BukkitConfiguration(File file, NameStyle nameStyle, CommentStyle commentStyle, boolean automaticColorStrings) {
+ this.file = file;
+ this.nameStyle = nameStyle;
+ this.commentStyle = commentStyle;
+ this.automaticColorStrings = automaticColorStrings;
+ this.cache = new HashMap<>();
+ this.comments = new HashMap<>();
+
+ try {
+ this.yamlOptions = (DumperOptions) yamlOptionsField.get(this);
+ this.yamlRepresenter = (YamlRepresenter) yamlRepresenterField.get(this);
+ this.yaml = (Yaml) yamlField.get(this);
+ } catch (IllegalAccessException e) {
+ throw new InvalidConfigException("Could not get values of Fields in YamlConfiguration", e);
+ }
+
+ this.copyDefaultConfig();
+ this.load();
+ }
+
+ public void set(String path, Object value, Comment comment) {
+ if (comment != null) {
+ this.comments.put(path, comment.value());
+ }
+
+ this.set(path, value);
+ }
+
+ @Override
+ public void set(String path, Object value) {
+ if (value != null && value.getClass().isArray()) {
+ value = Arrays.asList(((Object[]) value));
+ }
+
+ // Workaround for setting and loading empty List and Map.
+ if ((value instanceof List && ((List>) value).size() == 0) || (value instanceof Map && ((Map, ?>) value).size() == 0)) {
+ super.set(path, new ArrayList<>());
+ return;
+ }
+
+ if (value == null || TypeUtils.isSimpleType(value)) {
+ super.set(path, value);
+
+ if (value == null) {
+ if (this.cache.containsKey(path)) {
+ this.cache.put(path, null);
+ }
+
+ return;
+ }
+
+ if (value.getClass().equals(String.class) && this.automaticColorStrings) {
+ this.cache.put(path, ConversionUtils.fixColors(value.toString()));
+ }
+
+ return;
+ }
+
+ Serializer> serializer = Serializers.of(value);
+ if (serializer == null) {
+ throw new MissingSerializerException(value);
+ }
+
+ this.cache.put(path, value);
+ serializer.serialize(path, value, this);
+ }
+
+ @Override
+ public Object get(String path) {
+ return this.cache.containsKey(path) ? this.cache.get(path) : super.get(path);
+ }
+
+ @Override
+ public String saveToString() {
+ this.yamlOptions.setIndent(this.options().indent());
+ this.yamlOptions.setDefaultFlowStyle(DumperOptions.FlowStyle.BLOCK);
+ this.yamlOptions.setAllowUnicode(true);
+ this.yamlRepresenter.setDefaultFlowStyle(DumperOptions.FlowStyle.BLOCK);
+
+ String header = this.buildHeader();
+ String dump = yaml.dump(this.getValues(false));
+ if (dump.equals("{}\n")) {
+ dump = "";
+ }
+
+ List lines = new ArrayList<>();
+ for (String line : (header + dump).split("\n")) {
+ if (!line.contains(":") || line.startsWith(" ")) {
+ lines.add(line);
+ continue;
+ }
+
+ String key = line.split(":")[0];
+ String comment = this.comments.get(key);
+ if (comment != null) {
+ if (this.commentStyle == CommentStyle.ABOVE_CONTENT) {
+ lines.add("# " + comment);
+ } else {
+ lines.add(line + " # " + comment);
+ }
+
+ continue;
+ }
+
+ lines.add(line);
+ }
+
+ return String.join("\n", lines);
+ }
+
+ public void load() {
+ try {
+ this.cache.clear();
+ super.load(this.file);
+ } catch (IOException | InvalidConfigurationException e) {
+ throw new InvalidConfigException("Could not load config file (name: " + this.file.getName() + ")", e);
+ }
+ }
+
+ public void save() {
+ try {
+ this.save(this.file);
+ } catch (IOException e) {
+ throw new InvalidConfigException("Could not save config file (name: " + this.file.getName() + ")", e);
+ }
+ }
+
+ private void copyDefaultConfig() {
+ try {
+ if (!ConfigAPI.getPlugin().getDataFolder().exists()) {
+ ConfigAPI.getPlugin().getDataFolder().mkdir();
+ }
+
+ if (this.file.exists()) {
+ return;
+ }
+
+ InputStream input = ConfigAPI.getPlugin().getResource(file.getName());
+ if (input == null) {
+ this.file.createNewFile();
+ return;
+ }
+
+ OutputStream output = new FileOutputStream(file);
+ byte[] buf = new byte[1024];
+ int len;
+
+ while ((len = input.read(buf)) > 0) {
+ output.write(buf, 0, len);
+ }
+
+ output.close();
+ input.close();
+ } catch (IOException e) {
+ throw new InvalidConfigException("Could not save default file", e);
+ }
+ }
+
+ public void addToCache(String path, Object value) {
+ this.cache.put(path, value);
+ }
+
+ public File getFile() {
+ return file;
+ }
+
+ public NameStyle getNameStyle() {
+ return nameStyle;
+ }
+
+ public CommentStyle getCommentStyle() {
+ return commentStyle;
+ }
+
+ public Map getCache() {
+ return cache;
+ }
+}
diff --git a/src/main/java/pl/mikigal/config/Config.java b/src/main/java/pl/mikigal/config/Config.java
new file mode 100644
index 0000000..44da30d
--- /dev/null
+++ b/src/main/java/pl/mikigal/config/Config.java
@@ -0,0 +1,5 @@
+package pl.mikigal.config;
+
+public interface Config {
+ BukkitConfiguration getBukkitConfiguration();
+}
diff --git a/src/main/java/pl/mikigal/config/ConfigAPI.java b/src/main/java/pl/mikigal/config/ConfigAPI.java
new file mode 100644
index 0000000..4b0222a
--- /dev/null
+++ b/src/main/java/pl/mikigal/config/ConfigAPI.java
@@ -0,0 +1,76 @@
+package pl.mikigal.config;
+
+import org.bukkit.plugin.java.JavaPlugin;
+import pl.mikigal.config.annotation.ConfigName;
+import pl.mikigal.config.exception.InvalidConfigException;
+import pl.mikigal.config.serializer.Serializer;
+import pl.mikigal.config.serializer.Serializers;
+import pl.mikigal.config.style.CommentStyle;
+import pl.mikigal.config.style.NameStyle;
+
+import java.io.File;
+import java.lang.reflect.Proxy;
+import java.util.HashMap;
+import java.util.Map;
+
+public class ConfigAPI {
+
+ private static JavaPlugin plugin;
+ private static final Map configurations = new HashMap<>();
+ private static final Map rawConfigurations = new HashMap<>();
+
+ public static T init(Class clazz, NameStyle nameStyle, CommentStyle commentStyle, boolean automaticColorStrings, JavaPlugin plugin) {
+ ConfigAPI.plugin = plugin;
+ ConfigName configName = clazz.getAnnotation(ConfigName.class);
+ if (configName == null) {
+ throw new InvalidConfigException("Config must have annotation ConfigName with file's name");
+ }
+
+ String name = configName.value() + (configName.value().endsWith(".yml") ? "" : ".yml");
+ File file = new File(plugin.getDataFolder(), name);
+
+ BukkitConfiguration rawConfiguration = new BukkitConfiguration(file, nameStyle, commentStyle, automaticColorStrings);
+ rawConfigurations.put(name, rawConfiguration);
+
+ T configuration = (T) Proxy.newProxyInstance(clazz.getClassLoader(), new Class[]{clazz}, new ConfigInvocationHandler(clazz, rawConfiguration, automaticColorStrings));
+ configurations.put(name, configuration);
+
+ return configuration;
+ }
+
+ public static BukkitConfiguration getRawConfiguration(String name) {
+ return rawConfigurations.get(name.endsWith(".yml") ? name : name + ".yml");
+ }
+
+ public static BukkitConfiguration getRawConfiguration(Class extends Config> config) {
+ ConfigName configName = config.getAnnotation(ConfigName.class);
+ if (configName == null) {
+ throw new InvalidConfigException("Config must have annotation ConfigName with file's name");
+ }
+
+ String name = configName.value() + (configName.value().endsWith(".yml") ? "" : ".yml");
+ return rawConfigurations.get(name);
+ }
+
+ public static Config getConfiguration(String name) {
+ return configurations.get(name.endsWith(".yml") ? name : name + ".yml");
+ }
+
+ public static T getConfiguration(Class config) {
+ ConfigName configName = config.getAnnotation(ConfigName.class);
+ if (configName == null) {
+ throw new InvalidConfigException("Config must have annotation ConfigName with file's name");
+ }
+
+ String name = configName.value() + (configName.value().endsWith(".yml") ? "" : ".yml");
+ return (T) configurations.get(name);
+ }
+
+ public static void registerSerializer(Class> clazz, Serializer> serializer) {
+ Serializers.register(clazz, serializer);
+ }
+
+ public static JavaPlugin getPlugin() {
+ return plugin;
+ }
+}
diff --git a/src/main/java/pl/mikigal/config/ConfigInvocationHandler.java b/src/main/java/pl/mikigal/config/ConfigInvocationHandler.java
new file mode 100644
index 0000000..f103212
--- /dev/null
+++ b/src/main/java/pl/mikigal/config/ConfigInvocationHandler.java
@@ -0,0 +1,240 @@
+package pl.mikigal.config;
+
+import pl.mikigal.config.annotation.Comment;
+import pl.mikigal.config.annotation.ConfigOptional;
+import pl.mikigal.config.annotation.ConfigPath;
+import pl.mikigal.config.exception.InvalidConfigException;
+import pl.mikigal.config.exception.InvalidConfigFileException;
+import pl.mikigal.config.exception.MissingSerializerException;
+import pl.mikigal.config.serializer.Serializer;
+import pl.mikigal.config.serializer.Serializers;
+import pl.mikigal.config.util.ConversionUtils;
+import pl.mikigal.config.util.ReflectionUtils;
+import pl.mikigal.config.util.TypeUtils;
+
+import java.lang.reflect.InvocationHandler;
+import java.lang.reflect.Method;
+import java.lang.reflect.ParameterizedType;
+import java.util.HashMap;
+import java.util.Map;
+
+public class ConfigInvocationHandler implements InvocationHandler {
+
+ private final Class extends Config> clazz;
+ private final Map configPaths;
+ private final BukkitConfiguration configuration;
+ private final boolean automaticColorStrings;
+
+ public ConfigInvocationHandler(Class extends Config> clazz, BukkitConfiguration configuration, boolean automaticColorStrings) {
+ this.clazz = clazz;
+ this.configPaths = new HashMap<>();
+ this.configuration = configuration;
+ this.automaticColorStrings = automaticColorStrings;
+
+ this.prepareMethods();
+ if (this.updateConfigFile()) {
+ this.configuration.load();
+ this.prepareMethods();
+ }
+
+ this.validateConfig();
+ }
+
+ @Override
+ public Object invoke(Object proxy, Method method, Object[] args) {
+ String name = method.getName();
+ if (name.equals("getBukkitConfiguration")) {
+ return this.configuration;
+ } else if (name.equals("toString")) {
+ return this.clazz.toString();
+ } else if (name.equals("hashCode")) {
+ return this.hashCode();
+ } else if (name.equals("equals")) {
+ return proxy == args[0];
+ } else if (name.startsWith("get")) {
+ return this.processGetter(method);
+ } else if (name.startsWith("set")) {
+ this.processSetter(method, args);
+ return null;
+ }
+
+ return null;
+ }
+
+ private Object processGetter(Method method) {
+ if (!method.getName().startsWith("get")) {
+ return null;
+ }
+
+ String path = this.getConfigPath(method);
+ if (method.getReturnType().equals(String.class) && this.automaticColorStrings) {
+ String cache = (String) this.configuration.get(path);
+ if (cache == null) {
+ throw new InvalidConfigFileException("Variable in config (path: " + path + ") is required, but is not set");
+ }
+
+ if (!this.configuration.getCache().containsKey(path)) {
+ cache = ConversionUtils.fixColors(cache);
+ this.configuration.addToCache(path, cache);
+ }
+
+ return cache;
+ }
+
+ if (TypeUtils.isSimpleType(method)) {
+ Object value = this.configuration.get(path);
+ if (value == null) {
+ if (!method.isAnnotationPresent(ConfigOptional.class)) {
+ throw new InvalidConfigFileException("Variable in config (path: " + path + ") is required, but is not set");
+ }
+
+ return null;
+ }
+
+
+ if (!method.getReturnType().isInstance(value) && !value.getClass().equals(TypeUtils.getWrapper(method.getReturnType()))) {
+ throw new InvalidConfigException("Method " + method.getName() + " does not return type same as variable in config (path: " + path + "; " + value.getClass() + ")");
+ }
+
+ return value;
+ }
+
+ Serializer> serializer = Serializers.of(method.getReturnType());
+ if (serializer == null) {
+ throw new MissingSerializerException(method.getReturnType());
+ }
+
+ Object cache = this.configuration.get(path);
+ if (cache == null) {
+ throw new InvalidConfigFileException("Variable in config (path: " + path + ") is required, but is not set");
+ }
+
+ if (!serializer.getSerializerType().equals(cache.getClass())) {
+ cache = serializer.deserialize(path, this.configuration);
+ this.configuration.addToCache(path, cache);
+ }
+
+ return cache;
+ }
+
+ private void processSetter(Method method, Object[] args) {
+ if (!method.getName().startsWith("set")) {
+ return;
+ }
+
+ Object value = args[0];
+ if (value == null && !method.isAnnotationPresent(ConfigOptional.class)) {
+ throw new InvalidConfigException("You can't set value to config setter that isn't @ConfigOptional (method: " + method + ")");
+ }
+
+ configuration.set(this.getConfigPath(method), value, method.getAnnotation(Comment.class));
+ this.configuration.save();
+ }
+
+ private void prepareMethods() {
+ // Process getters
+ for (Method method : this.clazz.getDeclaredMethods()) {
+ String name = method.getName();
+ if (!name.startsWith("get")) {
+ continue;
+ }
+
+ if (!method.isDefault()) {
+ throw new InvalidConfigException("Getter method " + name + " has not default value");
+ }
+
+ if (Map.class.isAssignableFrom(method.getReturnType())) {
+ ParameterizedType returnTypes = (ParameterizedType) method.getGenericReturnType();
+ if (!returnTypes.getActualTypeArguments()[0].equals(String.class)) {
+ throw new InvalidConfigException("You can serialize Map only with String key");
+ }
+ }
+
+ ConfigPath configPath = method.getAnnotation(ConfigPath.class);
+ this.configPaths.put(name, configPath == null ? configuration.getNameStyle().format(name) : configPath.value());
+ }
+
+ // Process setters
+ for (Method method : this.clazz.getDeclaredMethods()) {
+ String name = method.getName();
+ if (!name.startsWith("set")) {
+ continue;
+ }
+
+ if (method.isDefault()) {
+ throw new InvalidConfigException("Setter method " + name + " has default value");
+ }
+
+ if (!method.getReturnType().equals(void.class)) {
+ throw new InvalidConfigException("Setter method " + name + " is not void type");
+ }
+
+ if (method.getParameterCount() != 1) {
+ throw new InvalidConfigException("Setter method " + name + " has not 1 parameter");
+ }
+
+ if (method.isAnnotationPresent(ConfigOptional.class)) {
+ throw new InvalidConfigException("Setter method " + name + " has ConfigOptional annotation");
+ }
+
+ String getter = name.replace("set", "get");
+ if (!this.configPaths.containsKey(getter)) {
+ throw new InvalidConfigException("Setter method " + name + " has not getter");
+ }
+
+ try {
+ if (!this.clazz.getDeclaredMethod(getter).getReturnType().equals(method.getParameters()[0].getType())) {
+ throw new InvalidConfigException("Setter method " + name + " has another parameter type than getter");
+ }
+ } catch (NoSuchMethodException e) {
+ throw new InvalidConfigException("Setter method " + name + " has not getter");
+ }
+
+ ConfigPath configPath = method.getAnnotation(ConfigPath.class);
+ this.configPaths.put(name, configPath == null ? configuration.getNameStyle().format(name) : configPath.value());
+ }
+ }
+
+ private boolean updateConfigFile() {
+ boolean modified = false;
+ Object proxy = ReflectionUtils.createHelperProxy(this.clazz);
+ for (Method method : this.clazz.getDeclaredMethods()) {
+ String name = method.getName();
+ if (!name.startsWith("get") && !name.startsWith("set")) {
+ throw new InvalidConfigException("Found non getter/setter method (name: " + name + ") in " + clazz.getCanonicalName());
+ }
+
+ if (!method.isDefault() || this.configuration.contains(this.getConfigPath(method))) {
+ continue;
+ }
+
+ Object defaultValue = ReflectionUtils.getDefaultValue(proxy, method);
+ if (defaultValue == null) {
+ if (!method.isAnnotationPresent(ConfigOptional.class)) {
+ throw new InvalidConfigException("Method " + method.getName() + " is not optional, but it's default value is null");
+ }
+
+ continue;
+ }
+
+ modified = true;
+ this.configuration.set(this.getConfigPath(method), defaultValue, method.getAnnotation(Comment.class));
+ }
+
+ if (modified) {
+ this.configuration.save();
+ }
+
+ return modified;
+ }
+
+ private void validateConfig() {
+ for (Method method : this.clazz.getDeclaredMethods()) {
+ this.processGetter(method);
+ }
+ }
+
+ private String getConfigPath(Method method) {
+ return this.configPaths.get(method.getName());
+ }
+}
diff --git a/src/main/java/pl/mikigal/config/annotation/Comment.java b/src/main/java/pl/mikigal/config/annotation/Comment.java
new file mode 100644
index 0000000..b494660
--- /dev/null
+++ b/src/main/java/pl/mikigal/config/annotation/Comment.java
@@ -0,0 +1,12 @@
+package pl.mikigal.config.annotation;
+
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+@Target({ElementType.METHOD})
+@Retention(value = RetentionPolicy.RUNTIME)
+public @interface Comment {
+ String value();
+}
diff --git a/src/main/java/pl/mikigal/config/annotation/ConfigName.java b/src/main/java/pl/mikigal/config/annotation/ConfigName.java
new file mode 100644
index 0000000..99d91fe
--- /dev/null
+++ b/src/main/java/pl/mikigal/config/annotation/ConfigName.java
@@ -0,0 +1,12 @@
+package pl.mikigal.config.annotation;
+
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+@Target({ElementType.TYPE})
+@Retention(value = RetentionPolicy.RUNTIME)
+public @interface ConfigName {
+ String value();
+}
diff --git a/src/main/java/pl/mikigal/config/annotation/ConfigOptional.java b/src/main/java/pl/mikigal/config/annotation/ConfigOptional.java
new file mode 100644
index 0000000..c10efad
--- /dev/null
+++ b/src/main/java/pl/mikigal/config/annotation/ConfigOptional.java
@@ -0,0 +1,11 @@
+package pl.mikigal.config.annotation;
+
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+@Target({ElementType.METHOD})
+@Retention(value = RetentionPolicy.RUNTIME)
+public @interface ConfigOptional {
+}
diff --git a/src/main/java/pl/mikigal/config/annotation/ConfigPath.java b/src/main/java/pl/mikigal/config/annotation/ConfigPath.java
new file mode 100644
index 0000000..be610d1
--- /dev/null
+++ b/src/main/java/pl/mikigal/config/annotation/ConfigPath.java
@@ -0,0 +1,12 @@
+package pl.mikigal.config.annotation;
+
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+@Target({ElementType.METHOD})
+@Retention(value = RetentionPolicy.RUNTIME)
+public @interface ConfigPath {
+ String value();
+}
diff --git a/src/main/java/pl/mikigal/config/exception/InvalidConfigException.java b/src/main/java/pl/mikigal/config/exception/InvalidConfigException.java
new file mode 100644
index 0000000..a51901e
--- /dev/null
+++ b/src/main/java/pl/mikigal/config/exception/InvalidConfigException.java
@@ -0,0 +1,16 @@
+package pl.mikigal.config.exception;
+
+public class InvalidConfigException extends RuntimeException {
+
+ public InvalidConfigException(String message) {
+ super(message + " It's probably issue with plugin, contact developer for support");
+ }
+
+ public InvalidConfigException(Throwable throwable) {
+ super(throwable);
+ }
+
+ public InvalidConfigException(String message, Throwable throwable) {
+ super(message, throwable);
+ }
+}
diff --git a/src/main/java/pl/mikigal/config/exception/InvalidConfigFileException.java b/src/main/java/pl/mikigal/config/exception/InvalidConfigFileException.java
new file mode 100644
index 0000000..d628835
--- /dev/null
+++ b/src/main/java/pl/mikigal/config/exception/InvalidConfigFileException.java
@@ -0,0 +1,8 @@
+package pl.mikigal.config.exception;
+
+public class InvalidConfigFileException extends RuntimeException {
+
+ public InvalidConfigFileException(String message) {
+ super(message + " It's probably issue with your config file");
+ }
+}
diff --git a/src/main/java/pl/mikigal/config/exception/MissingSerializerException.java b/src/main/java/pl/mikigal/config/exception/MissingSerializerException.java
new file mode 100644
index 0000000..7d9bb03
--- /dev/null
+++ b/src/main/java/pl/mikigal/config/exception/MissingSerializerException.java
@@ -0,0 +1,16 @@
+package pl.mikigal.config.exception;
+
+public class MissingSerializerException extends InvalidConfigException {
+
+ public MissingSerializerException(Class> clazz) {
+ super("Serializer for " + clazz + " does not exists. Did you forget to register it?");
+ }
+
+ public MissingSerializerException(String clazz) {
+ super("Class " + clazz + " does not exists, can't get Serializer of it");
+ }
+
+ public MissingSerializerException(Object object) {
+ this(object.getClass());
+ }
+}
diff --git a/src/main/java/pl/mikigal/config/serializer/ItemStackSerializer.java b/src/main/java/pl/mikigal/config/serializer/ItemStackSerializer.java
new file mode 100644
index 0000000..2e3bfb9
--- /dev/null
+++ b/src/main/java/pl/mikigal/config/serializer/ItemStackSerializer.java
@@ -0,0 +1,93 @@
+package pl.mikigal.config.serializer;
+
+import org.bukkit.Material;
+import org.bukkit.configuration.ConfigurationSection;
+import org.bukkit.enchantments.Enchantment;
+import org.bukkit.inventory.ItemStack;
+import org.bukkit.inventory.meta.ItemMeta;
+import pl.mikigal.config.BukkitConfiguration;
+import pl.mikigal.config.exception.InvalidConfigFileException;
+import pl.mikigal.config.util.ConversionUtils;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Map;
+
+public class ItemStackSerializer extends Serializer {
+
+ @Override
+ protected void saveObject(String path, ItemStack object, BukkitConfiguration configuration) {
+ configuration.set(path + ".material", object.getType().toString());
+ configuration.set(path + ".amount", object.getAmount());
+ if (object.getDurability() != 0) {
+ configuration.set(path + ".durability", object.getDurability());
+ }
+
+ System.out.println(object.getEnchantments());
+ for (Map.Entry entry : object.getEnchantments().entrySet()) {
+ System.out.println(entry);
+ configuration.set(path + ".enchantments." + entry.getKey().getName(), entry.getValue());
+ }
+
+ ItemMeta itemMeta = object.getItemMeta();
+ if (itemMeta == null) {
+ return;
+ }
+
+ if (itemMeta.getDisplayName() != null) {
+ configuration.set(path + ".name", itemMeta.getDisplayName().replace("§", "&"));
+ }
+
+ if (itemMeta.getLore() != null && itemMeta.getLore().size() != 0) {
+ List raw = new ArrayList<>();
+ for (String line : itemMeta.getLore()) {
+ raw.add(line.replace("§", "&"));
+ }
+
+ configuration.set(path + ".lore", raw);
+ }
+
+ object.setItemMeta(itemMeta);
+ }
+
+ @Override
+ public ItemStack deserialize(String path, BukkitConfiguration configuration) {
+ ConfigurationSection section = configuration.getConfigurationSection(path);
+
+ String rawMaterial = section.getString("material");
+ if (rawMaterial == null) {
+ throw new InvalidConfigFileException("Invalid material (" + rawMaterial + ") in ItemStack (path: " + section.getName() + ")");
+ }
+
+ Material material = Material.getMaterial(rawMaterial);
+ if (material == null) {
+ throw new InvalidConfigFileException("Invalid material (" + rawMaterial + ") in ItemStack (path: " + section.getName() + ")");
+ }
+
+ int amount = section.contains("amount") ? section.getInt("amount") : 1;
+ short durability = section.contains("durability") ? (short) section.getInt("durability") : 0;
+ String name = section.getString("name");
+ List lore = section.getStringList("lore");
+
+ ItemStack itemStack = new ItemStack(material, amount, durability);
+ ItemMeta itemMeta = itemStack.getItemMeta();
+ if (name != null) {
+ itemMeta.setDisplayName(ConversionUtils.fixColors(name));
+ }
+
+ if (lore != null && lore.size() != 0) {
+ itemMeta.setLore(ConversionUtils.fixColors(lore));
+ }
+
+ itemStack.setItemMeta(itemMeta);
+
+ if (section.contains("enchantments")) {
+ ConfigurationSection enchantments = section.getConfigurationSection("enchantments");
+ for (String key : enchantments.getKeys(false)) {
+ itemStack.addUnsafeEnchantment(Enchantment.getByName(key), enchantments.getInt(key));
+ }
+ }
+
+ return itemStack;
+ }
+}
diff --git a/src/main/java/pl/mikigal/config/serializer/LocationSerializer.java b/src/main/java/pl/mikigal/config/serializer/LocationSerializer.java
new file mode 100644
index 0000000..884decc
--- /dev/null
+++ b/src/main/java/pl/mikigal/config/serializer/LocationSerializer.java
@@ -0,0 +1,39 @@
+package pl.mikigal.config.serializer;
+
+import org.bukkit.Bukkit;
+import org.bukkit.Location;
+import org.bukkit.World;
+import org.bukkit.configuration.ConfigurationSection;
+import pl.mikigal.config.BukkitConfiguration;
+import pl.mikigal.config.exception.InvalidConfigFileException;
+import pl.mikigal.config.util.ConversionUtils;
+
+public class LocationSerializer extends Serializer {
+
+ @Override
+ protected void saveObject(String path, Location object, BukkitConfiguration configuration) {
+ configuration.set(path + ".world", object.getWorld().getName());
+ configuration.set(path + ".x", ConversionUtils.round(object.getX()));
+ configuration.set(path + ".y", ConversionUtils.round(object.getY()));
+ configuration.set(path + ".z", ConversionUtils.round(object.getZ()));
+ configuration.set(path + ".yaw", ConversionUtils.round(object.getYaw()));
+ configuration.set(path + ".pitch", ConversionUtils.round(object.getPitch()));
+ }
+
+ @Override
+ public Location deserialize(String path, BukkitConfiguration configuration) {
+ ConfigurationSection section = configuration.getConfigurationSection(path);
+ World world = Bukkit.getWorld(section.getString("world"));
+ if (world == null) {
+ throw new InvalidConfigFileException("Invalid Location (path: " + section.getName() + "), world " + section.getString("world") + " does not exist");
+ }
+
+ return new Location(
+ world,
+ section.getDouble("x"),
+ section.getDouble("y"),
+ section.getDouble("z"),
+ section.contains("yaw") ? (float) section.getDouble("yaw") : 0,
+ section.contains("pitch") ? (float) section.getDouble("pitch") : 0);
+ }
+}
diff --git a/src/main/java/pl/mikigal/config/serializer/PotionEffectSerializer.java b/src/main/java/pl/mikigal/config/serializer/PotionEffectSerializer.java
new file mode 100644
index 0000000..d165ba7
--- /dev/null
+++ b/src/main/java/pl/mikigal/config/serializer/PotionEffectSerializer.java
@@ -0,0 +1,29 @@
+package pl.mikigal.config.serializer;
+
+import org.bukkit.potion.PotionEffect;
+import org.bukkit.potion.PotionEffectType;
+import pl.mikigal.config.BukkitConfiguration;
+import pl.mikigal.config.exception.InvalidConfigFileException;
+
+public class PotionEffectSerializer extends Serializer {
+
+ @Override
+ protected void saveObject(String path, PotionEffect object, BukkitConfiguration configuration) {
+ configuration.set(path + ".type", object.getType().getName());
+ configuration.set(path + ".duration", object.getDuration());
+ configuration.set(path + ".amplifier", object.getAmplifier());
+ }
+
+ @Override
+ public PotionEffect deserialize(String path, BukkitConfiguration configuration) {
+ PotionEffectType type = PotionEffectType.getByName(configuration.getString(path + ".type"));
+ int duration = configuration.getInt(path + ".duration");
+ int amplifier = configuration.getInt(path + ".amplifier");
+
+ if (type == null) {
+ throw new InvalidConfigFileException("Invalid PotionEffect type (path: " + path + ")");
+ }
+
+ return new PotionEffect(type, duration, amplifier);
+ }
+}
diff --git a/src/main/java/pl/mikigal/config/serializer/Serializer.java b/src/main/java/pl/mikigal/config/serializer/Serializer.java
new file mode 100644
index 0000000..9cb150e
--- /dev/null
+++ b/src/main/java/pl/mikigal/config/serializer/Serializer.java
@@ -0,0 +1,37 @@
+package pl.mikigal.config.serializer;
+
+import pl.mikigal.config.BukkitConfiguration;
+import pl.mikigal.config.exception.InvalidConfigException;
+
+import java.lang.reflect.ParameterizedType;
+
+public abstract class Serializer {
+
+ private final Class serializerType;
+
+ public Serializer() {
+ ParameterizedType type = (ParameterizedType) this.getClass().getGenericSuperclass();
+ if (!(type.getActualTypeArguments()[0] instanceof Class)) {
+ throw new InvalidConfigException("Serializer can't have wildcard in generic");
+ }
+
+ this.serializerType = (Class) type.getActualTypeArguments()[0];
+ }
+
+ public final void serialize(String path, Object object, BukkitConfiguration configuration) {
+ configuration.set(path, null);
+ if (object == null) {
+ return;
+ }
+
+ this.saveObject(path, (T) object, configuration);
+ }
+
+ protected abstract void saveObject(String path, T object, BukkitConfiguration configuration);
+
+ public abstract T deserialize(String path, BukkitConfiguration configuration);
+
+ public Class getSerializerType() {
+ return serializerType;
+ }
+}
diff --git a/src/main/java/pl/mikigal/config/serializer/Serializers.java b/src/main/java/pl/mikigal/config/serializer/Serializers.java
new file mode 100644
index 0000000..9e979dd
--- /dev/null
+++ b/src/main/java/pl/mikigal/config/serializer/Serializers.java
@@ -0,0 +1,76 @@
+package pl.mikigal.config.serializer;
+
+import org.bukkit.Location;
+import org.bukkit.inventory.ItemStack;
+import org.bukkit.inventory.ShapedRecipe;
+import org.bukkit.potion.PotionEffect;
+import pl.mikigal.config.exception.InvalidConfigException;
+import pl.mikigal.config.exception.MissingSerializerException;
+import pl.mikigal.config.serializer.universal.UniversalListSerializer;
+import pl.mikigal.config.serializer.universal.UniversalMapSerializer;
+
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.UUID;
+
+public class Serializers {
+
+ public static final Map, Serializer>> SERIALIZERS = new HashMap<>();
+
+ static {
+ register(ItemStack.class, new ItemStackSerializer());
+ register(Location.class, new LocationSerializer());
+ register(ShapedRecipe.class, new ShapedRecipeSerializer());
+ register(PotionEffect.class, new PotionEffectSerializer());
+ register(UUID.class, new UUIDSerializer());
+
+ register(List.class, new UniversalListSerializer());
+ register(Map.class, new UniversalMapSerializer());
+ }
+
+ public static Serializer of(Class clazz) {
+ if (SERIALIZERS.containsKey(clazz)) {
+ return (Serializer) SERIALIZERS.get(clazz);
+ }
+
+ for (Map.Entry, Serializer>> entry : SERIALIZERS.entrySet()) {
+ if (entry.getKey().isAssignableFrom(clazz)) {
+ return (Serializer) entry.getValue();
+ }
+ }
+
+ return null;
+ }
+
+ public static Serializer of(T type) {
+ if (SERIALIZERS.containsKey(type.getClass())) {
+ return (Serializer) SERIALIZERS.get(type.getClass());
+ }
+
+ for (Map.Entry, Serializer>> entry : SERIALIZERS.entrySet()) {
+ if (entry.getKey().isAssignableFrom(type.getClass())) {
+ return (Serializer) entry.getValue();
+ }
+ }
+
+ return null;
+ }
+
+ public static Serializer> of(String classPath) {
+ try {
+ Class> clazz = Class.forName(classPath);
+ return of(clazz);
+ } catch (ClassNotFoundException e) {
+ throw new MissingSerializerException(classPath);
+ }
+ }
+
+ public static void register(Class> clazz, Serializer> serializer) {
+ if (!clazz.equals(serializer.getSerializerType())) {
+ throw new InvalidConfigException("Can't register serializer " + serializer.getClass().getName() + "! You tried to register it for another Class than it's generic type");
+ }
+
+ SERIALIZERS.put(clazz, serializer);
+ }
+}
diff --git a/src/main/java/pl/mikigal/config/serializer/ShapedRecipeSerializer.java b/src/main/java/pl/mikigal/config/serializer/ShapedRecipeSerializer.java
new file mode 100644
index 0000000..4aeee02
--- /dev/null
+++ b/src/main/java/pl/mikigal/config/serializer/ShapedRecipeSerializer.java
@@ -0,0 +1,95 @@
+package pl.mikigal.config.serializer;
+
+import org.bukkit.Material;
+import org.bukkit.configuration.ConfigurationSection;
+import org.bukkit.inventory.ItemStack;
+import org.bukkit.inventory.ShapedRecipe;
+import org.bukkit.plugin.Plugin;
+import pl.mikigal.config.BukkitConfiguration;
+import pl.mikigal.config.ConfigAPI;
+import pl.mikigal.config.exception.InvalidConfigException;
+import pl.mikigal.config.util.ReflectionUtils;
+
+import java.lang.reflect.Constructor;
+import java.lang.reflect.InvocationTargetException;
+import java.util.*;
+
+public class ShapedRecipeSerializer extends Serializer {
+
+ // Workaround to don't require both version to build
+ private static Constructor newVersionsConstructor;
+ private static Constructor> namespacedKeyConstructor;
+
+ static {
+ if (ReflectionUtils.isNewVersion()) {
+ try {
+ Class> namespacedKeyClass = Class.forName("org.bukkit.NamespacedKey");
+ newVersionsConstructor = ShapedRecipe.class.getConstructor(namespacedKeyClass, ItemStack.class);
+ namespacedKeyConstructor = namespacedKeyClass.getConstructor(Plugin.class, String.class);
+ } catch (NoSuchMethodException | ClassNotFoundException e) {
+ throw new InvalidConfigException("Could not find constructor for ShapedRecipe for new versions", e);
+ }
+ }
+ }
+
+ @Override
+ protected void saveObject(String path, ShapedRecipe object, BukkitConfiguration configuration) {
+ Serializers.of(ItemStack.class).serialize(path + ".result", object.getResult(), configuration);
+ List shape = new ArrayList<>();
+ for (String line : object.getShape()) {
+ for (char ingredient : line.toCharArray()) {
+ shape.add(ingredient);
+ }
+ }
+
+ for (int i = 0; i < 9; i++) {
+ char ingredient = shape.get(i);
+ if (ingredient == ' ') {
+ continue;
+ }
+
+ if (object.getIngredientMap().get(ingredient) == null) {
+ throw new InvalidConfigException("Invalid ShapedRecipe, there's no defined ingredient for char '" + ingredient + "'");
+ }
+
+ configuration.set(path + "." + i, object.getIngredientMap().get(ingredient).getType().toString());
+ }
+ }
+
+ @Override
+ public ShapedRecipe deserialize(String path, BukkitConfiguration configuration) {
+ ConfigurationSection section = configuration.getConfigurationSection(path);
+ ItemStack result = Serializers.of(ItemStack.class).deserialize(path + ".result", configuration);
+ ShapedRecipe recipe = ReflectionUtils.isNewVersion() ? this.createForNewVersion(result) : new ShapedRecipe(result);
+
+ Map ingredients = new HashMap<>();
+ for (int i = 0; i < 9; i++) {
+ if (!section.contains(String.valueOf(i))) {
+ continue;
+ }
+
+ ingredients.put(i, Material.valueOf(section.getString(String.valueOf(i))));
+ }
+
+ String shape = "";
+ for (int i = 0; i < 9; i++) {
+ shape += ingredients.containsKey(i) ? i : " ";
+ }
+
+ recipe = recipe.shape(shape.substring(0, 3), shape.substring(3, 6), shape.substring(6));
+
+ for (Map.Entry ingredient : ingredients.entrySet()) {
+ recipe = recipe.setIngredient(Character.forDigit(ingredient.getKey(), 10), ingredient.getValue());
+ }
+
+ return recipe;
+ }
+
+ private ShapedRecipe createForNewVersion(ItemStack result) {
+ try {
+ return newVersionsConstructor.newInstance(namespacedKeyConstructor.newInstance(ConfigAPI.getPlugin(), UUID.randomUUID().toString().substring(6)), result);
+ } catch (InstantiationException | IllegalAccessException | InvocationTargetException e) {
+ throw new InvalidConfigException("Could not create ShapedRecipe for new version");
+ }
+ }
+}
diff --git a/src/main/java/pl/mikigal/config/serializer/UUIDSerializer.java b/src/main/java/pl/mikigal/config/serializer/UUIDSerializer.java
new file mode 100644
index 0000000..31b6346
--- /dev/null
+++ b/src/main/java/pl/mikigal/config/serializer/UUIDSerializer.java
@@ -0,0 +1,18 @@
+package pl.mikigal.config.serializer;
+
+import pl.mikigal.config.BukkitConfiguration;
+
+import java.util.UUID;
+
+public class UUIDSerializer extends Serializer {
+
+ @Override
+ protected void saveObject(String path, UUID object, BukkitConfiguration configuration) {
+ configuration.set(path, object.toString());
+ }
+
+ @Override
+ public UUID deserialize(String path, BukkitConfiguration configuration) {
+ return UUID.fromString(configuration.getString(path));
+ }
+}
diff --git a/src/main/java/pl/mikigal/config/serializer/universal/UniversalListSerializer.java b/src/main/java/pl/mikigal/config/serializer/universal/UniversalListSerializer.java
new file mode 100644
index 0000000..682bf69
--- /dev/null
+++ b/src/main/java/pl/mikigal/config/serializer/universal/UniversalListSerializer.java
@@ -0,0 +1,63 @@
+package pl.mikigal.config.serializer.universal;
+
+import org.bukkit.configuration.ConfigurationSection;
+import pl.mikigal.config.BukkitConfiguration;
+import pl.mikigal.config.exception.InvalidConfigFileException;
+import pl.mikigal.config.exception.MissingSerializerException;
+import pl.mikigal.config.serializer.Serializer;
+import pl.mikigal.config.serializer.Serializers;
+import pl.mikigal.config.util.TypeUtils;
+
+import java.util.ArrayList;
+import java.util.List;
+
+public class UniversalListSerializer extends Serializer {
+
+ @Override
+ protected void saveObject(String path, List object, BukkitConfiguration configuration) {
+ Class> generic = TypeUtils.getListGeneric(object);
+ Serializer> serializer = Serializers.of(generic);
+ if (serializer == null) {
+ throw new MissingSerializerException(generic);
+ }
+
+ configuration.set(path + ".type", generic.getName());
+ for (int i = 0; i < object.size(); i++) {
+ serializer.serialize(path + "." + i, object.get(i), configuration);
+ }
+ }
+
+ @Override
+ public List> deserialize(String path, BukkitConfiguration configuration) {
+ ConfigurationSection section = configuration.getConfigurationSection(path);
+ if (section == null || section.getKeys(false).size() == 0) {
+ return new ArrayList<>();
+ }
+
+ String serializerClass = section.getString("type");
+ if (serializerClass == null) {
+ throw new InvalidConfigFileException("Serializer type is not defined for " + path);
+ }
+
+ Serializer> serializer = Serializers.of(serializerClass);
+
+ if (serializer == null) {
+ try {
+ throw new MissingSerializerException(Class.forName(serializerClass));
+ } catch (ClassNotFoundException e) {
+ throw new MissingSerializerException("Could not find class " + serializerClass);
+ }
+ }
+
+ List list = new ArrayList<>();
+ for (String index : section.getKeys(false)) {
+ if (index.equals("type")) {
+ continue;
+ }
+
+ list.add(serializer.deserialize(path + "." + index, configuration));
+ }
+
+ return list;
+ }
+}
diff --git a/src/main/java/pl/mikigal/config/serializer/universal/UniversalMapSerializer.java b/src/main/java/pl/mikigal/config/serializer/universal/UniversalMapSerializer.java
new file mode 100644
index 0000000..d32781f
--- /dev/null
+++ b/src/main/java/pl/mikigal/config/serializer/universal/UniversalMapSerializer.java
@@ -0,0 +1,80 @@
+package pl.mikigal.config.serializer.universal;
+
+import org.bukkit.configuration.ConfigurationSection;
+import pl.mikigal.config.BukkitConfiguration;
+import pl.mikigal.config.exception.InvalidConfigFileException;
+import pl.mikigal.config.exception.MissingSerializerException;
+import pl.mikigal.config.serializer.Serializer;
+import pl.mikigal.config.serializer.Serializers;
+import pl.mikigal.config.util.TypeUtils;
+
+import java.util.HashMap;
+import java.util.Map;
+
+public class UniversalMapSerializer extends Serializer