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 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 clazz; + private final Map configPaths; + private final BukkitConfiguration configuration; + private final boolean automaticColorStrings; + + public ConfigInvocationHandler(Class 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 { + + @Override + protected void saveObject(String path, Map object, BukkitConfiguration configuration) { + Class generic = TypeUtils.getMapGeneric(object)[1]; + if (TypeUtils.isSimpleType(generic)) { + for (Map.Entry entry : ((Map) object).entrySet()) { + configuration.set(path + "." + entry.getKey(), entry.getValue()); + } + + return; + } + + Serializer serializer = Serializers.of(generic); + if (serializer == null) { + throw new MissingSerializerException(generic); + } + + configuration.set(path + ".type", generic.getName()); + for (Map.Entry entry : ((Map) object).entrySet()) { + serializer.serialize(path + "." + entry.getKey(), entry.getValue(), configuration); + } + } + + @Override + public Map deserialize(String path, BukkitConfiguration configuration) { + ConfigurationSection section = configuration.getConfigurationSection(path); + if (section == null || section.getKeys(false).size() == 0) { + return new HashMap<>(); + } + + if (!section.contains("type")) { + Map map = new HashMap<>(); + for (String key : section.getKeys(false)) { + map.put(key, section.get(key)); + } + + return map; + } + + 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); + } + } + + Map map = new HashMap<>(); + for (String key : section.getKeys(false)) { + if (key.equals("type")) { + continue; + } + + map.put(key, serializer.deserialize(path + "." + key, configuration)); + } + + return map; + } +} diff --git a/src/main/java/pl/mikigal/config/style/CommentStyle.java b/src/main/java/pl/mikigal/config/style/CommentStyle.java new file mode 100644 index 0000000..a0a2e4e --- /dev/null +++ b/src/main/java/pl/mikigal/config/style/CommentStyle.java @@ -0,0 +1,6 @@ +package pl.mikigal.config.style; + +public enum CommentStyle { + INLINE, + ABOVE_CONTENT +} diff --git a/src/main/java/pl/mikigal/config/style/NameStyle.java b/src/main/java/pl/mikigal/config/style/NameStyle.java new file mode 100644 index 0000000..a997393 --- /dev/null +++ b/src/main/java/pl/mikigal/config/style/NameStyle.java @@ -0,0 +1,19 @@ +package pl.mikigal.config.style; + +import com.google.common.base.CaseFormat; + +public enum NameStyle { + CAMEL_CASE(CaseFormat.LOWER_CAMEL), + UNDERSCORE(CaseFormat.LOWER_UNDERSCORE), + HYPHEN(CaseFormat.LOWER_HYPHEN); + + private final CaseFormat caseFormat; + + NameStyle(CaseFormat caseFormat) { + this.caseFormat = caseFormat; + } + + public String format(String methodName) { + return CaseFormat.UPPER_CAMEL.to(this.caseFormat, methodName.replace("get", "").replace("set", "")); + } +} diff --git a/src/main/java/pl/mikigal/config/util/ConversionUtils.java b/src/main/java/pl/mikigal/config/util/ConversionUtils.java new file mode 100644 index 0000000..d725aa0 --- /dev/null +++ b/src/main/java/pl/mikigal/config/util/ConversionUtils.java @@ -0,0 +1,29 @@ +package pl.mikigal.config.util; + +import org.bukkit.ChatColor; + +import java.util.ArrayList; +import java.util.List; + +public class ConversionUtils { + + public static double round(double value) { + long factor = (long) Math.pow(10, 2); + value = value * factor; + long tmp = Math.round(value); + return (double) tmp / factor; + } + + public static String fixColors(String raw) { + return ChatColor.translateAlternateColorCodes('&', raw); + } + + public static List fixColors(List raw) { + List colored = new ArrayList<>(); + for (String line : raw) { + colored.add(fixColors(line)); + } + + return colored; + } +} diff --git a/src/main/java/pl/mikigal/config/util/Item.java b/src/main/java/pl/mikigal/config/util/Item.java new file mode 100644 index 0000000..fe0241a --- /dev/null +++ b/src/main/java/pl/mikigal/config/util/Item.java @@ -0,0 +1,81 @@ +package pl.mikigal.config.util; + +import org.bukkit.Material; +import org.bukkit.enchantments.Enchantment; +import org.bukkit.inventory.ItemStack; +import org.bukkit.inventory.meta.ItemMeta; + +import java.util.Arrays; +import java.util.List; +import java.util.Map; + +public class Item { + + private final ItemMeta itemMeta; + private final ItemStack itemStack; + + public Item(ItemStack itemStack) { + this.itemStack = itemStack; + this.itemMeta = itemStack.getItemMeta(); + } + + public static Item of(Item item) { + return new Item(item.toItem()); + } + + public static Item of(ItemStack itemStack) { + return new Item(itemStack); + } + + public static Item of(Material material) { + return new Item(new ItemStack(material)); + } + + public static Item of(Material material, int amount) { + return new Item(new ItemStack(material, amount)); + } + + public static Item of(Material material, int amount, short data) { + return new Item(new ItemStack(material, amount, data)); + } + + public Item amount(int amount) { + this.itemStack.setAmount(amount); + return this; + } + + public Item durability(short durability) { + this.itemStack.setDurability(durability); + return this; + } + + public Item name(String name) { + this.itemMeta.setDisplayName(ConversionUtils.fixColors(name)); + return this; + } + + public Item lore(String... lore) { + this.itemMeta.setLore(ConversionUtils.fixColors(Arrays.asList(lore))); + return this; + } + + public Item lore(List lore) { + this.itemMeta.setLore(ConversionUtils.fixColors(lore)); + return this; + } + + public Item enchantment(Enchantment enchantment, int level) { + this.itemStack.addUnsafeEnchantment(enchantment, level); + return this; + } + + public Item enchantments(Map enchantments) { + this.itemStack.addUnsafeEnchantments(enchantments); + return this; + } + + public ItemStack toItem() { + this.itemStack.setItemMeta(this.itemMeta); + return this.itemStack; + } +} \ No newline at end of file diff --git a/src/main/java/pl/mikigal/config/util/ReflectionUtils.java b/src/main/java/pl/mikigal/config/util/ReflectionUtils.java new file mode 100644 index 0000000..6381195 --- /dev/null +++ b/src/main/java/pl/mikigal/config/util/ReflectionUtils.java @@ -0,0 +1,45 @@ +package pl.mikigal.config.util; + +import org.bukkit.Bukkit; +import pl.mikigal.config.exception.InvalidConfigException; + +import java.lang.invoke.MethodHandles; +import java.lang.reflect.Constructor; +import java.lang.reflect.Method; +import java.lang.reflect.Proxy; + +public class ReflectionUtils { + + private static final Constructor lookupConstructor; + + static { + try { + lookupConstructor = MethodHandles.Lookup.class.getDeclaredConstructor(Class.class, Integer.TYPE); + lookupConstructor.setAccessible(true); + } catch (NoSuchMethodException e) { + throw new InvalidConfigException("Could not get MethodHandles.Lookup constructor", e); + } + } + + public static Object getDefaultValue(Object proxy, Method method) { + try { + Class clazz = method.getDeclaringClass(); + return lookupConstructor.newInstance(clazz, MethodHandles.Lookup.PRIVATE) + .in(clazz) + .unreflectSpecial(method, clazz) + .bindTo(proxy) + .invoke(); + } catch (Throwable throwable) { + throw new InvalidConfigException(throwable); + } + } + + public static Object createHelperProxy(Class clazz) { + return Proxy.newProxyInstance(clazz.getClassLoader(), new Class[]{clazz}, (Object object, Method method, Object[] args) -> null); + } + + public static boolean isNewVersion() { + int version = Integer.parseInt(Bukkit.getBukkitVersion().split("\\.")[1].split("-")[0]); + return version >= 12; + } +} diff --git a/src/main/java/pl/mikigal/config/util/TypeUtils.java b/src/main/java/pl/mikigal/config/util/TypeUtils.java new file mode 100644 index 0000000..193f64c --- /dev/null +++ b/src/main/java/pl/mikigal/config/util/TypeUtils.java @@ -0,0 +1,103 @@ +package pl.mikigal.config.util; + +import pl.mikigal.config.exception.InvalidConfigException; + +import java.lang.reflect.Method; +import java.lang.reflect.ParameterizedType; +import java.lang.reflect.Type; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +public class TypeUtils { + + public static final Map, Class> WRAPPERS = new HashMap<>(); + + static { + WRAPPERS.put(boolean.class, Boolean.class); + WRAPPERS.put(int.class, Integer.class); + WRAPPERS.put(char.class, Character.class); + WRAPPERS.put(byte.class, Byte.class); + WRAPPERS.put(short.class, Short.class); + WRAPPERS.put(double.class, Double.class); + WRAPPERS.put(long.class, Long.class); + WRAPPERS.put(float.class, Float.class); + } + + public static boolean isSimpleType(Method method) { + Class type = method.getReturnType(); + if (isPrimitiveOrWrapper(type) || type.equals(String.class)) { + return true; + } + + if (!type.equals(List.class)) { // Map is manually implemented in Proxy and UniversalMapSerializer + return false; + } + + if (!(method.getGenericReturnType() instanceof ParameterizedType)) { + throw new InvalidConfigException("Could not get generic type of " + method.getName()); + } + + ParameterizedType returnTypes = (ParameterizedType) method.getGenericReturnType(); + Type generic = returnTypes.getActualTypeArguments()[0]; + if (!(generic instanceof Class)) { + throw new InvalidConfigException("Could not get generic type of " + method.getName() + ". Config's method generic type can't be wildcard"); + } + + return isSimpleType((Class) generic); + } + + public static boolean isSimpleType(Object object) { + if (isPrimitiveOrWrapper(object.getClass()) || object.getClass().equals(String.class)) { + return true; + } + + if (!(object instanceof List)) { + return false; + } + + List list = (List) object; + if (list.size() == 0) { + throw new InvalidConfigException("Can't get generic type of empty List"); + } + + Class generic = list.get(0).getClass(); + return isPrimitiveOrWrapper(generic) || generic.equals(String.class); + } + + public static Class getListGeneric(List list) { + if (list.size() == 0) { + throw new InvalidConfigException("Can't get generic type of empty List"); + } + + return list.get(0).getClass(); + } + + public static Class[] getMapGeneric(Map map) { + for (Map.Entry entry : map.entrySet()) { + return new Class[]{entry.getKey().getClass(), entry.getValue().getClass()}; + } + + throw new InvalidConfigException("Can't get generic type of empty Map"); + } + + public static boolean isSimpleType(Class type) { // It does not handle List + return isPrimitiveOrWrapper(type) || type.equals(String.class); + } + + public static boolean isPrimitiveOrWrapper(Class clazz) { + return clazz.isPrimitive() || + clazz.equals(Boolean.class) || + clazz.equals(Integer.class) || + clazz.equals(Character.class) || + clazz.equals(Byte.class) || + clazz.equals(Short.class) || + clazz.equals(Double.class) || + clazz.equals(Long.class) || + clazz.equals(Float.class); + } + + public static Class getWrapper(Class primitive) { + return WRAPPERS.get(primitive); + } +}