diff --git a/.github/workflows/gradle.yml b/.github/workflows/gradle.yml index 858bf09..b39b9ee 100644 --- a/.github/workflows/gradle.yml +++ b/.github/workflows/gradle.yml @@ -28,7 +28,7 @@ jobs: run: chmod +x gradlew - name: Build - run: ./gradlew shadowJar -x test + run: ./gradlew build shadowJar -x test env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} @@ -63,7 +63,7 @@ jobs: run: ./gradlew test env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - + - name: Create Coverage run: ./gradlew jacocoTestReport env: diff --git a/src/main/java/com/georgev22/api/yaml/Configuration.java b/src/main/java/com/georgev22/api/yaml/Configuration.java new file mode 100644 index 0000000..74b0230 --- /dev/null +++ b/src/main/java/com/georgev22/api/yaml/Configuration.java @@ -0,0 +1,84 @@ +package com.georgev22.api.yaml; + +import java.util.Map; + +/** + * Represents a source of configurable options and settings + */ +public interface Configuration extends ConfigurationSection { + /** + * Sets the default value of the given path as provided. + *

+ * If no source {@link Configuration} was provided as a default + * collection, then a new {@link MemoryConfiguration} will be created to + * hold the new default value. + *

+ * If value is null, the value will be removed from the default + * Configuration source. + * + * @param path Path of the value to set. + * @param value Value to set the default to. + * @throws IllegalArgumentException Thrown if path is null. + */ + public void addDefault(String path, Object value); + + /** + * Sets the default values of the given paths as provided. + *

+ * If no source {@link Configuration} was provided as a default + * collection, then a new {@link MemoryConfiguration} will be created to + * hold the new default values. + * + * @param defaults A map of Path->Values to add to defaults. + * @throws IllegalArgumentException Thrown if defaults is null. + */ + public void addDefaults(Map defaults); + + /** + * Sets the default values of the given paths as provided. + *

+ * If no source {@link Configuration} was provided as a default + * collection, then a new {@link MemoryConfiguration} will be created to + * hold the new default value. + *

+ * This method will not hold a reference to the specified Configuration, + * nor will it automatically update if that Configuration ever changes. If + * you require this, you should set the default source with {@link + * #setDefaults(Configuration)}. + * + * @param defaults A configuration holding a list of defaults to copy. + * @throws IllegalArgumentException Thrown if defaults is null or this. + */ + public void addDefaults(Configuration defaults); + + /** + * Sets the source of all default values for this {@link Configuration}. + *

+ * If a previous source was set, or previous default values were defined, + * then they will not be copied to the new source. + * + * @param defaults New source of default values for this configuration. + * @throws IllegalArgumentException Thrown if defaults is null or this. + */ + public void setDefaults(Configuration defaults); + + /** + * Gets the source {@link Configuration} for this configuration. + *

+ * If no configuration source was set, but default values were added, then + * a {@link MemoryConfiguration} will be returned. If no source was set + * and no defaults were set, then this method will return null. + * + * @return Configuration source for default values, or null if none exist. + */ + public Configuration getDefaults(); + + /** + * Gets the {@link ConfigurationOptions} for this {@link Configuration}. + *

+ * All setters through this method are chainable. + * + * @return Options for this configuration + */ + public ConfigurationOptions options(); +} diff --git a/src/main/java/com/georgev22/api/yaml/ConfigurationOptions.java b/src/main/java/com/georgev22/api/yaml/ConfigurationOptions.java new file mode 100644 index 0000000..71da055 --- /dev/null +++ b/src/main/java/com/georgev22/api/yaml/ConfigurationOptions.java @@ -0,0 +1,90 @@ +package com.georgev22.api.yaml; + +/** + * Various settings for controlling the input and output of a {@link + * Configuration} + */ +public class ConfigurationOptions { + private char pathSeparator = '.'; + private boolean copyDefaults = false; + private final Configuration configuration; + + protected ConfigurationOptions(Configuration configuration) { + this.configuration = configuration; + } + + /** + * Returns the {@link Configuration} that this object is responsible for. + * + * @return Parent configuration + */ + public Configuration configuration() { + return configuration; + } + + /** + * Gets the char that will be used to separate {@link + * ConfigurationSection}s + *

+ * This value does not affect how the {@link Configuration} is stored, + * only in how you access the data. The default value is '.'. + * + * @return Path separator + */ + public char pathSeparator() { + return pathSeparator; + } + + /** + * Sets the char that will be used to separate {@link + * ConfigurationSection}s + *

+ * This value does not affect how the {@link Configuration} is stored, + * only in how you access the data. The default value is '.'. + * + * @param value Path separator + * @return This object, for chaining + */ + public ConfigurationOptions pathSeparator(char value) { + this.pathSeparator = value; + return this; + } + + /** + * Checks if the {@link Configuration} should copy values from its default + * {@link Configuration} directly. + *

+ * If this is true, all values in the default Configuration will be + * directly copied, making it impossible to distinguish between values + * that were set and values that are provided by default. As a result, + * {@link ConfigurationSection#contains(java.lang.String)} will always + * return the same value as {@link + * ConfigurationSection#isSet(java.lang.String)}. The default value is + * false. + * + * @return Whether or not defaults are directly copied + */ + public boolean copyDefaults() { + return copyDefaults; + } + + /** + * Sets if the {@link Configuration} should copy values from its default + * {@link Configuration} directly. + *

+ * If this is true, all values in the default Configuration will be + * directly copied, making it impossible to distinguish between values + * that were set and values that are provided by default. As a result, + * {@link ConfigurationSection#contains(java.lang.String)} will always + * return the same value as {@link + * ConfigurationSection#isSet(java.lang.String)}. The default value is + * false. + * + * @param value Whether or not defaults are directly copied + * @return This object, for chaining + */ + public ConfigurationOptions copyDefaults(boolean value) { + this.copyDefaults = value; + return this; + } +} diff --git a/src/main/java/com/georgev22/api/yaml/ConfigurationSection.java b/src/main/java/com/georgev22/api/yaml/ConfigurationSection.java new file mode 100644 index 0000000..8505b6a --- /dev/null +++ b/src/main/java/com/georgev22/api/yaml/ConfigurationSection.java @@ -0,0 +1,696 @@ +package com.georgev22.api.yaml; + +import com.georgev22.api.yaml.serialization.ConfigurationSerializable; + +import java.util.List; +import java.util.Map; +import java.util.Set; + +/** + * Represents a section of a {@link Configuration} + */ +public interface ConfigurationSection { + /** + * Gets a set containing all keys in this section. + *

+ * If deep is set to true, then this will contain all the keys within any + * child {@link ConfigurationSection}s (and their children, etc). These + * will be in a valid path notation for you to use. + *

+ * If deep is set to false, then this will contain only the keys of any + * direct children, and not their own children. + * + * @param deep Whether or not to get a deep list, as opposed to a shallow + * list. + * @return Set of keys contained within this ConfigurationSection. + */ + public Set getKeys(boolean deep); + + /** + * Gets a Map containing all keys and their values for this section. + *

+ * If deep is set to true, then this will contain all the keys and values + * within any child {@link ConfigurationSection}s (and their children, + * etc). These keys will be in a valid path notation for you to use. + *

+ * If deep is set to false, then this will contain only the keys and + * values of any direct children, and not their own children. + * + * @param deep Whether or not to get a deep list, as opposed to a shallow + * list. + * @return Map of keys and values of this section. + */ + public Map getValues(boolean deep); + + /** + * Checks if this {@link ConfigurationSection} contains the given path. + *

+ * If the value for the requested path does not exist but a default value + * has been specified, this will return true. + * + * @param path Path to check for existence. + * @return True if this section contains the requested path, either via + * default or being set. + * @throws IllegalArgumentException Thrown when path is null. + */ + public boolean contains(String path); + + /** + * Checks if this {@link ConfigurationSection} contains the given path. + *

+ * If the value for the requested path does not exist, the boolean parameter + * of true has been specified, a default value for the path exists, this + * will return true. + *

+ * If a boolean parameter of false has been specified, true will only be + * returned if there is a set value for the specified path. + * + * @param path Path to check for existence. + * @param ignoreDefault Whether or not to ignore if a default value for the + * specified path exists. + * @return True if this section contains the requested path, or if a default + * value exist and the boolean parameter for this method is true. + * @throws IllegalArgumentException Thrown when path is null. + */ + public boolean contains(String path, boolean ignoreDefault); + + /** + * Checks if this {@link ConfigurationSection} has a value set for the + * given path. + *

+ * If the value for the requested path does not exist but a default value + * has been specified, this will still return false. + * + * @param path Path to check for existence. + * @return True if this section contains the requested path, regardless of + * having a default. + * @throws IllegalArgumentException Thrown when path is null. + */ + public boolean isSet(String path); + + /** + * Gets the path of this {@link ConfigurationSection} from its root {@link + * Configuration} + *

+ * For any {@link Configuration} themselves, this will return an empty + * string. + *

+ * If the section is no longer contained within its root for any reason, + * such as being replaced with a different value, this may return null. + *

+ * To retrieve the single name of this section, that is, the final part of + * the path returned by this method, you may use {@link #getName()}. + * + * @return Path of this section relative to its root + */ + public String getCurrentPath(); + + /** + * Gets the name of this individual {@link ConfigurationSection}, in the + * path. + *

+ * This will always be the final part of {@link #getCurrentPath()}, unless + * the section is orphaned. + * + * @return Name of this section + */ + public String getName(); + + /** + * Gets the root {@link Configuration} that contains this {@link + * ConfigurationSection} + *

+ * For any {@link Configuration} themselves, this will return its own + * object. + *

+ * If the section is no longer contained within its root for any reason, + * such as being replaced with a different value, this may return null. + * + * @return Root configuration containing this section. + */ + public Configuration getRoot(); + + /** + * Gets the parent {@link ConfigurationSection} that directly contains + * this {@link ConfigurationSection}. + *

+ * For any {@link Configuration} themselves, this will return null. + *

+ * If the section is no longer contained within its parent for any reason, + * such as being replaced with a different value, this may return null. + * + * @return Parent section containing this section. + */ + public ConfigurationSection getParent(); + + /** + * Gets the requested Object by path. + *

+ * If the Object does not exist but a default value has been specified, + * this will return the default value. If the Object does not exist and no + * default value was specified, this will return null. + * + * @param path Path of the Object to get. + * @return Requested Object. + */ + public Object get(String path); + + /** + * Gets the requested Object by path, returning a default value if not + * found. + *

+ * If the Object does not exist then the specified default value will + * returned regardless of if a default has been identified in the root + * {@link Configuration}. + * + * @param path Path of the Object to get. + * @param def The default value to return if the path is not found. + * @return Requested Object. + */ + public Object get(String path, Object def); + + /** + * Sets the specified path to the given value. + *

+ * If value is null, the entry will be removed. Any existing entry will be + * replaced, regardless of what the new value is. + *

+ * Some implementations may have limitations on what you may store. See + * their individual javadocs for details. No implementations should allow + * you to store {@link Configuration}s or {@link ConfigurationSection}s, + * please use {@link #createSection(java.lang.String)} for that. + * + * @param path Path of the object to set. + * @param value New value to set the path to. + */ + public void set(String path, Object value); + + /** + * Creates an empty {@link ConfigurationSection} at the specified path. + *

+ * Any value that was previously set at this path will be overwritten. If + * the previous value was itself a {@link ConfigurationSection}, it will + * be orphaned. + * + * @param path Path to create the section at. + * @return Newly created section + */ + public ConfigurationSection createSection(String path); + + /** + * Creates a {@link ConfigurationSection} at the specified path, with + * specified values. + *

+ * Any value that was previously set at this path will be overwritten. If + * the previous value was itself a {@link ConfigurationSection}, it will + * be orphaned. + * + * @param path Path to create the section at. + * @param map The values to used. + * @return Newly created section + */ + public ConfigurationSection createSection(String path, Map map); + + // Primitives + /** + * Gets the requested String by path. + *

+ * If the String does not exist but a default value has been specified, + * this will return the default value. If the String does not exist and no + * default value was specified, this will return null. + * + * @param path Path of the String to get. + * @return Requested String. + */ + public String getString(String path); + + /** + * Gets the requested String by path, returning a default value if not + * found. + *

+ * If the String does not exist then the specified default value will + * returned regardless of if a default has been identified in the root + * {@link Configuration}. + * + * @param path Path of the String to get. + * @param def The default value to return if the path is not found or is + * not a String. + * @return Requested String. + */ + public String getString(String path, String def); + + /** + * Checks if the specified path is a String. + *

+ * If the path exists but is not a String, this will return false. If the + * path does not exist, this will return false. If the path does not exist + * but a default value has been specified, this will check if that default + * value is a String and return appropriately. + * + * @param path Path of the String to check. + * @return Whether or not the specified path is a String. + */ + public boolean isString(String path); + + /** + * Gets the requested int by path. + *

+ * If the int does not exist but a default value has been specified, this + * will return the default value. If the int does not exist and no default + * value was specified, this will return 0. + * + * @param path Path of the int to get. + * @return Requested int. + */ + public int getInt(String path); + + /** + * Gets the requested int by path, returning a default value if not found. + *

+ * If the int does not exist then the specified default value will + * returned regardless of if a default has been identified in the root + * {@link Configuration}. + * + * @param path Path of the int to get. + * @param def The default value to return if the path is not found or is + * not an int. + * @return Requested int. + */ + public int getInt(String path, int def); + + /** + * Checks if the specified path is an int. + *

+ * If the path exists but is not a int, this will return false. If the + * path does not exist, this will return false. If the path does not exist + * but a default value has been specified, this will check if that default + * value is a int and return appropriately. + * + * @param path Path of the int to check. + * @return Whether or not the specified path is an int. + */ + public boolean isInt(String path); + + /** + * Gets the requested boolean by path. + *

+ * If the boolean does not exist but a default value has been specified, + * this will return the default value. If the boolean does not exist and + * no default value was specified, this will return false. + * + * @param path Path of the boolean to get. + * @return Requested boolean. + */ + public boolean getBoolean(String path); + + /** + * Gets the requested boolean by path, returning a default value if not + * found. + *

+ * If the boolean does not exist then the specified default value will + * returned regardless of if a default has been identified in the root + * {@link Configuration}. + * + * @param path Path of the boolean to get. + * @param def The default value to return if the path is not found or is + * not a boolean. + * @return Requested boolean. + */ + public boolean getBoolean(String path, boolean def); + + /** + * Checks if the specified path is a boolean. + *

+ * If the path exists but is not a boolean, this will return false. If the + * path does not exist, this will return false. If the path does not exist + * but a default value has been specified, this will check if that default + * value is a boolean and return appropriately. + * + * @param path Path of the boolean to check. + * @return Whether or not the specified path is a boolean. + */ + public boolean isBoolean(String path); + + /** + * Gets the requested double by path. + *

+ * If the double does not exist but a default value has been specified, + * this will return the default value. If the double does not exist and no + * default value was specified, this will return 0. + * + * @param path Path of the double to get. + * @return Requested double. + */ + public double getDouble(String path); + + /** + * Gets the requested double by path, returning a default value if not + * found. + *

+ * If the double does not exist then the specified default value will + * returned regardless of if a default has been identified in the root + * {@link Configuration}. + * + * @param path Path of the double to get. + * @param def The default value to return if the path is not found or is + * not a double. + * @return Requested double. + */ + public double getDouble(String path, double def); + + /** + * Checks if the specified path is a double. + *

+ * If the path exists but is not a double, this will return false. If the + * path does not exist, this will return false. If the path does not exist + * but a default value has been specified, this will check if that default + * value is a double and return appropriately. + * + * @param path Path of the double to check. + * @return Whether or not the specified path is a double. + */ + public boolean isDouble(String path); + + /** + * Gets the requested long by path. + *

+ * If the long does not exist but a default value has been specified, this + * will return the default value. If the long does not exist and no + * default value was specified, this will return 0. + * + * @param path Path of the long to get. + * @return Requested long. + */ + public long getLong(String path); + + /** + * Gets the requested long by path, returning a default value if not + * found. + *

+ * If the long does not exist then the specified default value will + * returned regardless of if a default has been identified in the root + * {@link Configuration}. + * + * @param path Path of the long to get. + * @param def The default value to return if the path is not found or is + * not a long. + * @return Requested long. + */ + public long getLong(String path, long def); + + /** + * Checks if the specified path is a long. + *

+ * If the path exists but is not a long, this will return false. If the + * path does not exist, this will return false. If the path does not exist + * but a default value has been specified, this will check if that default + * value is a long and return appropriately. + * + * @param path Path of the long to check. + * @return Whether or not the specified path is a long. + */ + public boolean isLong(String path); + + // Java + /** + * Gets the requested List by path. + *

+ * If the List does not exist but a default value has been specified, this + * will return the default value. If the List does not exist and no + * default value was specified, this will return null. + * + * @param path Path of the List to get. + * @return Requested List. + */ + public List getList(String path); + + /** + * Gets the requested List by path, returning a default value if not + * found. + *

+ * If the List does not exist then the specified default value will + * returned regardless of if a default has been identified in the root + * {@link Configuration}. + * + * @param path Path of the List to get. + * @param def The default value to return if the path is not found or is + * not a List. + * @return Requested List. + */ + public List getList(String path, List def); + + /** + * Checks if the specified path is a List. + *

+ * If the path exists but is not a List, this will return false. If the + * path does not exist, this will return false. If the path does not exist + * but a default value has been specified, this will check if that default + * value is a List and return appropriately. + * + * @param path Path of the List to check. + * @return Whether or not the specified path is a List. + */ + public boolean isList(String path); + + /** + * Gets the requested List of String by path. + *

+ * If the List does not exist but a default value has been specified, this + * will return the default value. If the List does not exist and no + * default value was specified, this will return an empty List. + *

+ * This method will attempt to cast any values into a String if possible, + * but may miss any values out if they are not compatible. + * + * @param path Path of the List to get. + * @return Requested List of String. + */ + public List getStringList(String path); + + /** + * Gets the requested List of Integer by path. + *

+ * If the List does not exist but a default value has been specified, this + * will return the default value. If the List does not exist and no + * default value was specified, this will return an empty List. + *

+ * This method will attempt to cast any values into a Integer if possible, + * but may miss any values out if they are not compatible. + * + * @param path Path of the List to get. + * @return Requested List of Integer. + */ + public List getIntegerList(String path); + + /** + * Gets the requested List of Boolean by path. + *

+ * If the List does not exist but a default value has been specified, this + * will return the default value. If the List does not exist and no + * default value was specified, this will return an empty List. + *

+ * This method will attempt to cast any values into a Boolean if possible, + * but may miss any values out if they are not compatible. + * + * @param path Path of the List to get. + * @return Requested List of Boolean. + */ + public List getBooleanList(String path); + + /** + * Gets the requested List of Double by path. + *

+ * If the List does not exist but a default value has been specified, this + * will return the default value. If the List does not exist and no + * default value was specified, this will return an empty List. + *

+ * This method will attempt to cast any values into a Double if possible, + * but may miss any values out if they are not compatible. + * + * @param path Path of the List to get. + * @return Requested List of Double. + */ + public List getDoubleList(String path); + + /** + * Gets the requested List of Float by path. + *

+ * If the List does not exist but a default value has been specified, this + * will return the default value. If the List does not exist and no + * default value was specified, this will return an empty List. + *

+ * This method will attempt to cast any values into a Float if possible, + * but may miss any values out if they are not compatible. + * + * @param path Path of the List to get. + * @return Requested List of Float. + */ + public List getFloatList(String path); + + /** + * Gets the requested List of Long by path. + *

+ * If the List does not exist but a default value has been specified, this + * will return the default value. If the List does not exist and no + * default value was specified, this will return an empty List. + *

+ * This method will attempt to cast any values into a Long if possible, + * but may miss any values out if they are not compatible. + * + * @param path Path of the List to get. + * @return Requested List of Long. + */ + public List getLongList(String path); + + /** + * Gets the requested List of Byte by path. + *

+ * If the List does not exist but a default value has been specified, this + * will return the default value. If the List does not exist and no + * default value was specified, this will return an empty List. + *

+ * This method will attempt to cast any values into a Byte if possible, + * but may miss any values out if they are not compatible. + * + * @param path Path of the List to get. + * @return Requested List of Byte. + */ + public List getByteList(String path); + + /** + * Gets the requested List of Character by path. + *

+ * If the List does not exist but a default value has been specified, this + * will return the default value. If the List does not exist and no + * default value was specified, this will return an empty List. + *

+ * This method will attempt to cast any values into a Character if + * possible, but may miss any values out if they are not compatible. + * + * @param path Path of the List to get. + * @return Requested List of Character. + */ + public List getCharacterList(String path); + + /** + * Gets the requested List of Short by path. + *

+ * If the List does not exist but a default value has been specified, this + * will return the default value. If the List does not exist and no + * default value was specified, this will return an empty List. + *

+ * This method will attempt to cast any values into a Short if possible, + * but may miss any values out if they are not compatible. + * + * @param path Path of the List to get. + * @return Requested List of Short. + */ + public List getShortList(String path); + + /** + * Gets the requested List of Maps by path. + *

+ * If the List does not exist but a default value has been specified, this + * will return the default value. If the List does not exist and no + * default value was specified, this will return an empty List. + *

+ * This method will attempt to cast any values into a Map if possible, but + * may miss any values out if they are not compatible. + * + * @param path Path of the List to get. + * @return Requested List of Maps. + */ + public List> getMapList(String path); + + // Bukkit + /** + * Gets the requested {@link ConfigurationSerializable} object at the given + * path. + * + * If the Object does not exist but a default value has been specified, this + * will return the default value. If the Object does not exist and no + * default value was specified, this will return null. + * + * @param the type of {@link ConfigurationSerializable} + * @param path the path to the object. + * @param clazz the type of {@link ConfigurationSerializable} + * @return Requested {@link ConfigurationSerializable} object + */ + public T getSerializable(String path, Class clazz); + + /** + * Gets the requested {@link ConfigurationSerializable} object at the given + * path, returning a default value if not found + * + * If the Object does not exist then the specified default value will + * returned regardless of if a default has been identified in the root + * {@link Configuration}. + * + * @param the type of {@link ConfigurationSerializable} + * @param path the path to the object. + * @param clazz the type of {@link ConfigurationSerializable} + * @param def the default object to return if the object is not present at + * the path + * @return Requested {@link ConfigurationSerializable} object + */ + public T getSerializable(String path, Class clazz, T def); + + /* + * Gets the requested ConfigurationSection by path. + *

+ * If the ConfigurationSection does not exist but a default value has been + * specified, this will return the default value. If the + * ConfigurationSection does not exist and no default value was specified, + * this will return null. + * + * @param path Path of the ConfigurationSection to get. + * @return Requested ConfigurationSection. + */ + public ConfigurationSection getConfigurationSection(String path); + + /** + * Checks if the specified path is a ConfigurationSection. + *

+ * If the path exists but is not a ConfigurationSection, this will return + * false. If the path does not exist, this will return false. If the path + * does not exist but a default value has been specified, this will check + * if that default value is a ConfigurationSection and return + * appropriately. + * + * @param path Path of the ConfigurationSection to check. + * @return Whether or not the specified path is a ConfigurationSection. + */ + public boolean isConfigurationSection(String path); + + /** + * Gets the equivalent {@link ConfigurationSection} from the default + * {@link Configuration} defined in {@link #getRoot()}. + *

+ * If the root contains no defaults, or the defaults doesn't contain a + * value for this path, or the value at this path is not a {@link + * ConfigurationSection} then this will return null. + * + * @return Equivalent section in root configuration + */ + public ConfigurationSection getDefaultSection(); + + /** + * Sets the default value in the root at the given path as provided. + *

+ * If no source {@link Configuration} was provided as a default + * collection, then a new {@link MemoryConfiguration} will be created to + * hold the new default value. + *

+ * If value is null, the value will be removed from the default + * Configuration source. + *

+ * If the value as returned by {@link #getDefaultSection()} is null, then + * this will create a new section at the path, replacing anything that may + * have existed there previously. + * + * @param path Path of the value to set. + * @param value Value to set the default to. + * @throws IllegalArgumentException Thrown if path is null. + */ + public void addDefault(String path, Object value); +} diff --git a/src/main/java/com/georgev22/api/yaml/InvalidConfigurationException.java b/src/main/java/com/georgev22/api/yaml/InvalidConfigurationException.java new file mode 100644 index 0000000..d98ed6a --- /dev/null +++ b/src/main/java/com/georgev22/api/yaml/InvalidConfigurationException.java @@ -0,0 +1,45 @@ +package com.georgev22.api.yaml; + +/** + * Exception thrown when attempting to load an invalid {@link Configuration} + */ +@SuppressWarnings("serial") +public class InvalidConfigurationException extends Exception { + + /** + * Creates a new instance of InvalidConfigurationException without a + * message or cause. + */ + public InvalidConfigurationException() {} + + /** + * Constructs an instance of InvalidConfigurationException with the + * specified message. + * + * @param msg The details of the exception. + */ + public InvalidConfigurationException(String msg) { + super(msg); + } + + /** + * Constructs an instance of InvalidConfigurationException with the + * specified cause. + * + * @param cause The cause of the exception. + */ + public InvalidConfigurationException(Throwable cause) { + super(cause); + } + + /** + * Constructs an instance of InvalidConfigurationException with the + * specified message and cause. + * + * @param cause The cause of the exception. + * @param msg The details of the exception. + */ + public InvalidConfigurationException(String msg, Throwable cause) { + super(msg, cause); + } +} diff --git a/src/main/java/com/georgev22/api/yaml/MemoryConfiguration.java b/src/main/java/com/georgev22/api/yaml/MemoryConfiguration.java new file mode 100644 index 0000000..bce4b86 --- /dev/null +++ b/src/main/java/com/georgev22/api/yaml/MemoryConfiguration.java @@ -0,0 +1,80 @@ +package com.georgev22.api.yaml; + +import com.georgev22.api.utilities.Utils; + +import java.util.Map; + +/** + * This is a {@link Configuration} implementation that does not save or load + * from any source, and stores all values in memory only. + * This is useful for temporary Configurations for providing defaults. + */ +public class MemoryConfiguration extends MemorySection implements Configuration { + protected Configuration defaults; + protected MemoryConfigurationOptions options; + + /** + * Creates an empty {@link MemoryConfiguration} with no default values. + */ + public MemoryConfiguration() { + } + + /** + * Creates an empty {@link MemoryConfiguration} using the specified {@link + * Configuration} as a source for all default values. + * + * @param defaults Default value provider + * @throws IllegalArgumentException Thrown if defaults is null + */ + public MemoryConfiguration(Configuration defaults) { + this.defaults = defaults; + } + + @Override + public void addDefault(String path, Object value) { + Utils.Assertions.notNull("Path may not be null", path); + + if (defaults == null) { + defaults = new MemoryConfiguration(); + } + + defaults.set(path, value); + } + + public void addDefaults(Map defaults) { + Utils.Assertions.notNull("Defaults may not be null", defaults); + + for (Map.Entry entry : defaults.entrySet()) { + addDefault(entry.getKey(), entry.getValue()); + } + } + + public void addDefaults(Configuration defaults) { + Utils.Assertions.notNull("Defaults may not be null", defaults); + + addDefaults(defaults.getValues(true)); + } + + public void setDefaults(Configuration defaults) { + Utils.Assertions.notNull("Defaults may not be null", defaults); + + this.defaults = defaults; + } + + public Configuration getDefaults() { + return defaults; + } + + @Override + public ConfigurationSection getParent() { + return null; + } + + public MemoryConfigurationOptions options() { + if (options == null) { + options = new MemoryConfigurationOptions(this); + } + + return options; + } +} diff --git a/src/main/java/com/georgev22/api/yaml/MemoryConfigurationOptions.java b/src/main/java/com/georgev22/api/yaml/MemoryConfigurationOptions.java new file mode 100644 index 0000000..aa84800 --- /dev/null +++ b/src/main/java/com/georgev22/api/yaml/MemoryConfigurationOptions.java @@ -0,0 +1,28 @@ +package com.georgev22.api.yaml; + +/** + * Various settings for controlling the input and output of a {@link + * MemoryConfiguration} + */ +public class MemoryConfigurationOptions extends ConfigurationOptions { + protected MemoryConfigurationOptions(MemoryConfiguration configuration) { + super(configuration); + } + + @Override + public MemoryConfiguration configuration() { + return (MemoryConfiguration) super.configuration(); + } + + @Override + public MemoryConfigurationOptions copyDefaults(boolean value) { + super.copyDefaults(value); + return this; + } + + @Override + public MemoryConfigurationOptions pathSeparator(char value) { + super.pathSeparator(value); + return this; + } +} diff --git a/src/main/java/com/georgev22/api/yaml/MemorySection.java b/src/main/java/com/georgev22/api/yaml/MemorySection.java new file mode 100644 index 0000000..d0e59e9 --- /dev/null +++ b/src/main/java/com/georgev22/api/yaml/MemorySection.java @@ -0,0 +1,776 @@ +package com.georgev22.api.yaml; + +import com.georgev22.api.utilities.Utils; +import com.georgev22.api.yaml.serialization.ConfigurationSerializable; +import org.jetbrains.annotations.NotNull; + +import java.util.ArrayList; +import java.util.LinkedHashMap; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; + +/** + * A type of {@link ConfigurationSection} that is stored in memory. + */ +public class MemorySection implements ConfigurationSection { + protected final Map map = new LinkedHashMap(); + private final Configuration root; + private final ConfigurationSection parent; + private final String path; + private final String fullPath; + + /** + * Creates an empty MemorySection for use as a root {@link Configuration} + * section. + *

+ * Note that calling this without being yourself a {@link Configuration} + * will throw an exception! + * + * @throws IllegalStateException Thrown if this is not a {@link + * Configuration} root. + */ + protected MemorySection() { + if (!(this instanceof Configuration)) { + throw new IllegalStateException("Cannot construct a root MemorySection when not a Configuration"); + } + + this.path = ""; + this.fullPath = ""; + this.parent = null; + this.root = (Configuration) this; + } + + /** + * Creates an empty MemorySection with the specified parent and path. + * + * @param parent Parent section that contains this own section. + * @param path Path that you may access this section from via the root + * {@link Configuration}. + * @throws IllegalArgumentException Thrown is parent or path is null, or + * if parent contains no root Configuration. + */ + protected MemorySection(ConfigurationSection parent, String path) { + Utils.Assertions.notNull("Parent cannot be null", parent); + Utils.Assertions.notNull("Path cannot be null", path); + + this.path = path; + this.parent = parent; + this.root = parent.getRoot(); + + Utils.Assertions.notNull("Path cannot be orphaned", root); + + this.fullPath = createPath(parent, path); + } + + public Set getKeys(boolean deep) { + Set result = new LinkedHashSet(); + + Configuration root = getRoot(); + if (root != null && root.options().copyDefaults()) { + ConfigurationSection defaults = getDefaultSection(); + + if (defaults != null) { + result.addAll(defaults.getKeys(deep)); + } + } + + mapChildrenKeys(result, this, deep); + + return result; + } + + public Map getValues(boolean deep) { + Map result = new LinkedHashMap(); + + Configuration root = getRoot(); + if (root != null && root.options().copyDefaults()) { + ConfigurationSection defaults = getDefaultSection(); + + if (defaults != null) { + result.putAll(defaults.getValues(deep)); + } + } + + mapChildrenValues(result, this, deep); + + return result; + } + + public boolean contains(String path) { + return contains(path, false); + } + + public boolean contains(String path, boolean ignoreDefault) { + return ((ignoreDefault) ? get(path, null) : get(path)) != null; + } + + public boolean isSet(String path) { + Configuration root = getRoot(); + if (root == null) { + return false; + } + if (root.options().copyDefaults()) { + return contains(path); + } + return get(path, null) != null; + } + + public String getCurrentPath() { + return fullPath; + } + + public String getName() { + return path; + } + + public Configuration getRoot() { + return root; + } + + public ConfigurationSection getParent() { + return parent; + } + + public void addDefault(String path, Object value) { + Utils.Assertions.notNull("Path cannot be null", path); + + Configuration root = getRoot(); + if (root == null) { + throw new IllegalStateException("Cannot add default without root"); + } + if (root == this) { + throw new UnsupportedOperationException("Unsupported addDefault(String, Object) implementation"); + } + root.addDefault(createPath(this, path), value); + } + + public ConfigurationSection getDefaultSection() { + Configuration root = getRoot(); + Configuration defaults = root == null ? null : root.getDefaults(); + + if (defaults != null) { + if (defaults.isConfigurationSection(getCurrentPath())) { + return defaults.getConfigurationSection(getCurrentPath()); + } + } + + return null; + } + + public void set(String path, Object value) { + Utils.Assertions.notEmpty(path, "Cannot set to an empty path"); + + Configuration root = getRoot(); + if (root == null) { + throw new IllegalStateException("Cannot use section without a root"); + } + + final char separator = root.options().pathSeparator(); + // i1 is the leading (higher) index + // i2 is the trailing (lower) index + int i1 = -1, i2; + ConfigurationSection section = this; + while ((i1 = path.indexOf(separator, i2 = i1 + 1)) != -1) { + String node = path.substring(i2, i1); + ConfigurationSection subSection = section.getConfigurationSection(node); + if (subSection == null) { + if (value == null) { + // no need to create missing sub-sections if we want to remove the value: + return; + } + section = section.createSection(node); + } else { + section = subSection; + } + } + + String key = path.substring(i2); + if (section == this) { + if (value == null) { + map.remove(key); + } else { + map.put(key, value); + } + } else { + section.set(key, value); + } + } + + public Object get(String path) { + return get(path, getDefault(path)); + } + + public Object get(String path, Object def) { + Utils.Assertions.notNull("Path cannot be null", path); + + if (path.length() == 0) { + return this; + } + + Configuration root = getRoot(); + if (root == null) { + throw new IllegalStateException("Cannot access section without a root"); + } + + final char separator = root.options().pathSeparator(); + // i1 is the leading (higher) index + // i2 is the trailing (lower) index + int i1 = -1, i2; + ConfigurationSection section = this; + while ((i1 = path.indexOf(separator, i2 = i1 + 1)) != -1) { + section = section.getConfigurationSection(path.substring(i2, i1)); + if (section == null) { + return def; + } + } + + String key = path.substring(i2); + if (section == this) { + Object result = map.get(key); + return (result == null) ? def : result; + } + return section.get(key, def); + } + + public ConfigurationSection createSection(String path) { + Utils.Assertions.notEmpty(path, "Cannot create section at empty path"); + Configuration root = getRoot(); + if (root == null) { + throw new IllegalStateException("Cannot create section without a root"); + } + + final char separator = root.options().pathSeparator(); + // i1 is the leading (higher) index + // i2 is the trailing (lower) index + int i1 = -1, i2; + ConfigurationSection section = this; + while ((i1 = path.indexOf(separator, i2 = i1 + 1)) != -1) { + String node = path.substring(i2, i1); + ConfigurationSection subSection = section.getConfigurationSection(node); + if (subSection == null) { + section = section.createSection(node); + } else { + section = subSection; + } + } + + String key = path.substring(i2); + if (section == this) { + ConfigurationSection result = new MemorySection(this, key); + map.put(key, result); + return result; + } + return section.createSection(key); + } + + public ConfigurationSection createSection(String path, Map map) { + ConfigurationSection section = createSection(path); + + for (Map.Entry entry : map.entrySet()) { + if (entry.getValue() instanceof Map) { + section.createSection(entry.getKey().toString(), (Map) entry.getValue()); + } else { + section.set(entry.getKey().toString(), entry.getValue()); + } + } + + return section; + } + + // Primitives + public String getString(String path) { + Object def = getDefault(path); + return getString(path, def != null ? def.toString() : null); + } + + public String getString(String path, String def) { + Object val = get(path, def); + return (val != null) ? val.toString() : def; + } + + public boolean isString(String path) { + Object val = get(path); + return val instanceof String; + } + + public int getInt(String path) { + Object def = getDefault(path); + return getInt(path, (def instanceof Number) ? Utils.toInt(def) : 0); + } + + public int getInt(String path, int def) { + Object val = get(path, def); + return (val instanceof Number) ? Utils.toInt(val) : def; + } + + public boolean isInt(String path) { + Object val = get(path); + return val instanceof Integer; + } + + public boolean getBoolean(String path) { + Object def = getDefault(path); + return getBoolean(path, (def instanceof Boolean) ? (Boolean) def : false); + } + + public boolean getBoolean(String path, boolean def) { + Object val = get(path, def); + return (val instanceof Boolean) ? (Boolean) val : def; + } + + public boolean isBoolean(String path) { + Object val = get(path); + return val instanceof Boolean; + } + + public double getDouble(String path) { + Object def = getDefault(path); + return getDouble(path, (def instanceof Number) ? Utils.toDouble(def) : 0); + } + + public double getDouble(String path, double def) { + Object val = get(path, def); + return (val instanceof Number) ? Utils.toDouble(val) : def; + } + + public boolean isDouble(String path) { + Object val = get(path); + return val instanceof Double; + } + + public long getLong(String path) { + Object def = getDefault(path); + return getLong(path, (def instanceof Number) ? Utils.toLong(def) : 0); + } + + public long getLong(String path, long def) { + Object val = get(path, def); + return (val instanceof Number) ? Utils.toLong(val) : def; + } + + public boolean isLong(String path) { + Object val = get(path); + return val instanceof Long; + } + + // Java + public List getList(String path) { + Object def = getDefault(path); + return getList(path, (def instanceof List) ? (List) def : null); + } + + public List getList(String path, List def) { + Object val = get(path, def); + return (List) ((val instanceof List) ? val : def); + } + + public boolean isList(String path) { + Object val = get(path); + return val instanceof List; + } + + public List getStringList(String path) { + List list = getList(path); + + if (list == null) { + return new ArrayList(0); + } + + List result = new ArrayList(); + + for (Object object : list) { + if ((object instanceof String) || (isPrimitiveWrapper(object))) { + result.add(String.valueOf(object)); + } + } + + return result; + } + + public List getIntegerList(String path) { + List list = getList(path); + + if (list == null) { + return new ArrayList(0); + } + + List result = new ArrayList(); + + for (Object object : list) { + if (object instanceof Integer) { + result.add((Integer) object); + } else if (object instanceof String) { + try { + result.add(Integer.valueOf((String) object)); + } catch (Exception ex) { + } + } else if (object instanceof Character) { + result.add((int) ((Character) object).charValue()); + } else if (object instanceof Number) { + result.add(((Number) object).intValue()); + } + } + + return result; + } + + public List getBooleanList(String path) { + List list = getList(path); + + if (list == null) { + return new ArrayList(0); + } + + List result = new ArrayList(); + + for (Object object : list) { + if (object instanceof Boolean) { + result.add((Boolean) object); + } else if (object instanceof String) { + if (Boolean.TRUE.toString().equals(object)) { + result.add(true); + } else if (Boolean.FALSE.toString().equals(object)) { + result.add(false); + } + } + } + + return result; + } + + public List getDoubleList(String path) { + List list = getList(path); + + if (list == null) { + return new ArrayList(0); + } + + List result = new ArrayList(); + + for (Object object : list) { + if (object instanceof Double) { + result.add((Double) object); + } else if (object instanceof String) { + try { + result.add(Double.valueOf((String) object)); + } catch (Exception ex) { + } + } else if (object instanceof Character) { + result.add((double) ((Character) object).charValue()); + } else if (object instanceof Number) { + result.add(((Number) object).doubleValue()); + } + } + + return result; + } + + public List getFloatList(String path) { + List list = getList(path); + + if (list == null) { + return new ArrayList(0); + } + + List result = new ArrayList(); + + for (Object object : list) { + if (object instanceof Float) { + result.add((Float) object); + } else if (object instanceof String) { + try { + result.add(Float.valueOf((String) object)); + } catch (Exception ex) { + } + } else if (object instanceof Character) { + result.add((float) ((Character) object).charValue()); + } else if (object instanceof Number) { + result.add(((Number) object).floatValue()); + } + } + + return result; + } + + public List getLongList(String path) { + List list = getList(path); + + if (list == null) { + return new ArrayList(0); + } + + List result = new ArrayList(); + + for (Object object : list) { + if (object instanceof Long) { + result.add((Long) object); + } else if (object instanceof String) { + try { + result.add(Long.valueOf((String) object)); + } catch (Exception ex) { + } + } else if (object instanceof Character) { + result.add((long) ((Character) object).charValue()); + } else if (object instanceof Number) { + result.add(((Number) object).longValue()); + } + } + + return result; + } + + public List getByteList(String path) { + List list = getList(path); + + if (list == null) { + return new ArrayList(0); + } + + List result = new ArrayList(); + + for (Object object : list) { + if (object instanceof Byte) { + result.add((Byte) object); + } else if (object instanceof String) { + try { + result.add(Byte.valueOf((String) object)); + } catch (Exception ex) { + } + } else if (object instanceof Character) { + result.add((byte) ((Character) object).charValue()); + } else if (object instanceof Number) { + result.add(((Number) object).byteValue()); + } + } + + return result; + } + + public List getCharacterList(String path) { + List list = getList(path); + + if (list == null) { + return new ArrayList(0); + } + + List result = new ArrayList(); + + for (Object object : list) { + if (object instanceof Character) { + result.add((Character) object); + } else if (object instanceof String) { + String str = (String) object; + + if (str.length() == 1) { + result.add(str.charAt(0)); + } + } else if (object instanceof Number) { + result.add((char) ((Number) object).intValue()); + } + } + + return result; + } + + public List getShortList(String path) { + List list = getList(path); + + if (list == null) { + return new ArrayList(0); + } + + List result = new ArrayList(); + + for (Object object : list) { + if (object instanceof Short) { + result.add((Short) object); + } else if (object instanceof String) { + try { + result.add(Short.valueOf((String) object)); + } catch (Exception ex) { + } + } else if (object instanceof Character) { + result.add((short) ((Character) object).charValue()); + } else if (object instanceof Number) { + result.add(((Number) object).shortValue()); + } + } + + return result; + } + + public List> getMapList(String path) { + List list = getList(path); + List> result = new ArrayList>(); + + if (list == null) { + return result; + } + + for (Object object : list) { + if (object instanceof Map) { + result.add((Map) object); + } + } + + return result; + } + + // Bukkit + @Override + public T getSerializable(String path, Class clazz) { + Utils.Assertions.notNull("ConfigurationSerializable class cannot be null", clazz); + Object def = getDefault(path); + return getSerializable(path, clazz, (def != null && clazz.isInstance(def)) ? clazz.cast(def) : null); + } + + @Override + public T getSerializable(String path, Class clazz, T def) { + Utils.Assertions.notNull("ConfigurationSerializable class cannot be null", clazz); + Object val = get(path); + return (val != null && clazz.isInstance(val)) ? clazz.cast(val) : def; + } + + public ConfigurationSection getConfigurationSection(String path) { + Object val = get(path, null); + if (val != null) { + return (val instanceof ConfigurationSection) ? (ConfigurationSection) val : null; + } + + val = get(path, getDefault(path)); + return (val instanceof ConfigurationSection) ? createSection(path) : null; + } + + public boolean isConfigurationSection(String path) { + Object val = get(path); + return val instanceof ConfigurationSection; + } + + protected boolean isPrimitiveWrapper(Object input) { + return input instanceof Integer || input instanceof Boolean || + input instanceof Character || input instanceof Byte || + input instanceof Short || input instanceof Double || + input instanceof Long || input instanceof Float; + } + + protected Object getDefault(String path) { + Utils.Assertions.notNull("Path cannot be null", path); + + Configuration root = getRoot(); + Configuration defaults = root == null ? null : root.getDefaults(); + return (defaults == null) ? null : defaults.get(createPath(this, path)); + } + + protected void mapChildrenKeys(Set output, ConfigurationSection section, boolean deep) { + if (section instanceof MemorySection) { + MemorySection sec = (MemorySection) section; + + for (Map.Entry entry : sec.map.entrySet()) { + output.add(createPath(section, entry.getKey(), this)); + + if ((deep) && (entry.getValue() instanceof ConfigurationSection)) { + ConfigurationSection subsection = (ConfigurationSection) entry.getValue(); + mapChildrenKeys(output, subsection, deep); + } + } + } else { + Set keys = section.getKeys(deep); + + for (String key : keys) { + output.add(createPath(section, key, this)); + } + } + } + + protected void mapChildrenValues(Map output, ConfigurationSection section, boolean deep) { + if (section instanceof MemorySection) { + MemorySection sec = (MemorySection) section; + + for (Map.Entry entry : sec.map.entrySet()) { + output.put(createPath(section, entry.getKey(), this), entry.getValue()); + + if (entry.getValue() instanceof ConfigurationSection) { + if (deep) { + mapChildrenValues(output, (ConfigurationSection) entry.getValue(), deep); + } + } + } + } else { + Map values = section.getValues(deep); + + for (Map.Entry entry : values.entrySet()) { + output.put(createPath(section, entry.getKey(), this), entry.getValue()); + } + } + } + + /** + * Creates a full path to the given {@link ConfigurationSection} from its + * root {@link Configuration}. + *

+ * You may use this method for any given {@link ConfigurationSection}, not + * only {@link MemorySection}. + * + * @param section Section to create a path for. + * @param key Name of the specified section. + * @return Full path of the section from its root. + */ + public static String createPath(ConfigurationSection section, String key) { + return createPath(section, key, (section == null) ? null : section.getRoot()); + } + + /** + * Creates a relative path to the given {@link ConfigurationSection} from + * the given relative section. + *

+ * You may use this method for any given {@link ConfigurationSection}, not + * only {@link MemorySection}. + * + * @param section Section to create a path for. + * @param key Name of the specified section. + * @param relativeTo Section to create the path relative to. + * @return Full path of the section from its root. + */ + public static @NotNull String createPath(ConfigurationSection section, String key, ConfigurationSection relativeTo) { + Utils.Assertions.notNull("Cannot create path without a section", section); + Configuration root = section.getRoot(); + if (root == null) { + throw new IllegalStateException("Cannot create path without a root"); + } + char separator = root.options().pathSeparator(); + + StringBuilder builder = new StringBuilder(); + if (section != null) { + for (ConfigurationSection parent = section; (parent != null) && (parent != relativeTo); parent = parent.getParent()) { + if (builder.length() > 0) { + builder.insert(0, separator); + } + + builder.insert(0, parent.getName()); + } + } + + if ((key != null) && (key.length() > 0)) { + if (builder.length() > 0) { + builder.append(separator); + } + + builder.append(key); + } + + return builder.toString(); + } + + @Override + public String toString() { + Configuration root = getRoot(); + return getClass().getSimpleName() + + "[path='" + + getCurrentPath() + + "', root='" + + (root == null ? null : root.getClass().getSimpleName()) + + "']"; + } +} diff --git a/src/main/java/com/georgev22/api/yaml/file/FileConfiguration.java b/src/main/java/com/georgev22/api/yaml/file/FileConfiguration.java new file mode 100644 index 0000000..92e1456 --- /dev/null +++ b/src/main/java/com/georgev22/api/yaml/file/FileConfiguration.java @@ -0,0 +1,236 @@ +package com.georgev22.api.yaml.file; + +import java.io.BufferedReader; +import java.io.File; +import java.io.FileInputStream; +import java.io.FileNotFoundException; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.InputStreamReader; +import java.io.OutputStreamWriter; +import java.io.Reader; +import java.io.Writer; +import java.nio.charset.StandardCharsets; + +import com.georgev22.api.utilities.Utils; +import com.georgev22.api.yaml.Configuration; +import com.georgev22.api.yaml.InvalidConfigurationException; +import com.georgev22.api.yaml.MemoryConfiguration; +import org.jetbrains.annotations.NotNull; + +/** + * This is a base class for all File based implementations of {@link + * Configuration} + */ +public abstract class FileConfiguration extends MemoryConfiguration { + + /** + * Creates an empty {@link FileConfiguration} with no default values. + */ + public FileConfiguration() { + super(); + } + + /** + * Creates an empty {@link FileConfiguration} using the specified {@link + * Configuration} as a source for all default values. + * + * @param defaults Default value provider + */ + public FileConfiguration(Configuration defaults) { + super(defaults); + } + + /** + * Saves this {@link FileConfiguration} to the specified location. + *

+ * If the file does not exist, it will be created. If already exists, it + * will be overwritten. If it cannot be overwritten or created, an + * exception will be thrown. + *

+ * This method will save using the system default encoding, or possibly + * using UTF8. + * + * @param file File to save to. + * @throws IOException Thrown when the given file cannot be written to for + * any reason. + * @throws IllegalArgumentException Thrown when file is null. + */ + public void save(File file) throws IOException { + Utils.Assertions.notNull("File cannot be null", file); + + createParentDirs(file); + + String data = saveToString(); + + try (Writer writer = new OutputStreamWriter(new FileOutputStream(file), StandardCharsets.UTF_8)) { + writer.write(data); + } + } + + private void createParentDirs(@NotNull File file) throws IOException { + File parent = file.getCanonicalFile().getParentFile(); + if (parent == null) { + /* + * The given directory is a filesystem root. All zero of its ancestors + * exist. This doesn't mean that the root itself exists -- consider x:\ on + * a Windows machine without such a drive -- or even that the caller can + * create it, but this method makes no such guarantees even for non-root + * files. + */ + return; + } + parent.mkdirs(); + if (!parent.isDirectory()) { + throw new IOException("Unable to create parent directories of " + file); + } + } + + /** + * Saves this {@link FileConfiguration} to the specified location. + *

+ * If the file does not exist, it will be created. If already exists, it + * will be overwritten. If it cannot be overwritten or created, an + * exception will be thrown. + *

+ * This method will save using the system default encoding, or possibly + * using UTF8. + * + * @param file File to save to. + * @throws IOException Thrown when the given file cannot be written to for + * any reason. + * @throws IllegalArgumentException Thrown when file is null. + */ + public void save(String file) throws IOException { + Utils.Assertions.notNull("File cannot be null", file); + + save(new File(file)); + } + + /** + * Saves this {@link FileConfiguration} to a string, and returns it. + * + * @return String containing this configuration. + */ + public abstract String saveToString(); + + /** + * Loads this {@link FileConfiguration} from the specified location. + *

+ * All the values contained within this configuration will be removed, + * leaving only settings and defaults, and the new values will be loaded + * from the given file. + *

+ * If the file cannot be loaded for any reason, an exception will be + * thrown. + * + * @param file File to load from. + * @throws FileNotFoundException Thrown when the given file cannot be + * opened. + * @throws IOException Thrown when the given file cannot be read. + * @throws InvalidConfigurationException Thrown when the given file is not + * a valid Configuration. + * @throws IllegalArgumentException Thrown when file is null. + */ + public void load(File file) throws FileNotFoundException, IOException, InvalidConfigurationException { + Utils.Assertions.notNull("File cannot be null", file); + + final FileInputStream stream = new FileInputStream(file); + + load(new InputStreamReader(stream, StandardCharsets.UTF_8)); + } + + /** + * Loads this {@link FileConfiguration} from the specified reader. + *

+ * All the values contained within this configuration will be removed, + * leaving only settings and defaults, and the new values will be loaded + * from the given stream. + * + * @param reader the reader to load from + * @throws IOException thrown when underlying reader throws an IOException + * @throws InvalidConfigurationException thrown when the reader does not + * represent a valid Configuration + * @throws IllegalArgumentException thrown when reader is null + */ + public void load(Reader reader) throws IOException, InvalidConfigurationException { + BufferedReader input = reader instanceof BufferedReader ? (BufferedReader) reader : new BufferedReader(reader); + + StringBuilder builder = new StringBuilder(); + + try { + String line; + + while ((line = input.readLine()) != null) { + builder.append(line); + builder.append('\n'); + } + } finally { + input.close(); + } + + loadFromString(builder.toString()); + } + + /** + * Loads this {@link FileConfiguration} from the specified location. + *

+ * All the values contained within this configuration will be removed, + * leaving only settings and defaults, and the new values will be loaded + * from the given file. + *

+ * If the file cannot be loaded for any reason, an exception will be + * thrown. + * + * @param file File to load from. + * @throws FileNotFoundException Thrown when the given file cannot be + * opened. + * @throws IOException Thrown when the given file cannot be read. + * @throws InvalidConfigurationException Thrown when the given file is not + * a valid Configuration. + * @throws IllegalArgumentException Thrown when file is null. + */ + public void load(String file) throws FileNotFoundException, IOException, InvalidConfigurationException { + Utils.Assertions.notNull("File cannot be null", file); + + load(new File(file)); + } + + /** + * Loads this {@link FileConfiguration} from the specified string, as + * opposed to from file. + *

+ * All the values contained within this configuration will be removed, + * leaving only settings and defaults, and the new values will be loaded + * from the given string. + *

+ * If the string is invalid in any way, an exception will be thrown. + * + * @param contents Contents of a Configuration to load. + * @throws InvalidConfigurationException Thrown if the specified string is + * invalid. + * @throws IllegalArgumentException Thrown if contents is null. + */ + public abstract void loadFromString(String contents) throws InvalidConfigurationException; + + /** + * Compiles the header for this {@link FileConfiguration} and returns the + * result. + *

+ * This will use the header from {@link #options()} -> {@link + * FileConfigurationOptions#header()}, respecting the rules of {@link + * FileConfigurationOptions#copyHeader()} if set. + * + * @return Compiled header + */ + protected abstract String buildHeader(); + + @Override + public FileConfigurationOptions options() { + if (options == null) { + options = new FileConfigurationOptions(this); + } + + return (FileConfigurationOptions) options; + } +} \ No newline at end of file diff --git a/src/main/java/com/georgev22/api/yaml/file/FileConfigurationOptions.java b/src/main/java/com/georgev22/api/yaml/file/FileConfigurationOptions.java new file mode 100644 index 0000000..09a829b --- /dev/null +++ b/src/main/java/com/georgev22/api/yaml/file/FileConfigurationOptions.java @@ -0,0 +1,120 @@ +package com.georgev22.api.yaml.file; + +import com.georgev22.api.yaml.Configuration; +import com.georgev22.api.yaml.MemoryConfiguration; +import com.georgev22.api.yaml.MemoryConfigurationOptions; + +/** + * Various settings for controlling the input and output of a {@link + * FileConfiguration} + */ +public class FileConfigurationOptions extends MemoryConfigurationOptions { + private String header = null; + private boolean copyHeader = true; + + protected FileConfigurationOptions(MemoryConfiguration configuration) { + super(configuration); + } + + @Override + public FileConfiguration configuration() { + return (FileConfiguration) super.configuration(); + } + + @Override + public FileConfigurationOptions copyDefaults(boolean value) { + super.copyDefaults(value); + return this; + } + + @Override + public FileConfigurationOptions pathSeparator(char value) { + super.pathSeparator(value); + return this; + } + + /** + * Gets the header that will be applied to the top of the saved output. + *

+ * This header will be commented out and applied directly at the top of + * the generated output of the {@link FileConfiguration}. It is not + * required to include a newline at the end of the header as it will + * automatically be applied, but you may include one if you wish for extra + * spacing. + *

+ * Null is a valid value which will indicate that no header is to be + * applied. The default value is null. + * + * @return Header + */ + public String header() { + return header; + } + + /** + * Sets the header that will be applied to the top of the saved output. + *

+ * This header will be commented out and applied directly at the top of + * the generated output of the {@link FileConfiguration}. It is not + * required to include a newline at the end of the header as it will + * automatically be applied, but you may include one if you wish for extra + * spacing. + *

+ * Null is a valid value which will indicate that no header is to be + * applied. + * + * @param value New header + * @return This object, for chaining + */ + public FileConfigurationOptions header(String value) { + this.header = value; + return this; + } + + /** + * Gets whether or not the header should be copied from a default source. + *

+ * If this is true, if a default {@link FileConfiguration} is passed to + * {@link + * FileConfiguration#setDefaults(Configuration)} + * then upon saving it will use the header from that config, instead of + * the one provided here. + *

+ * If no default is set on the configuration, or the default is not of + * type FileConfiguration, or that config has no header ({@link #header()} + * returns null) then the header specified in this configuration will be + * used. + *

+ * Defaults to true. + * + * @return Whether or not to copy the header + */ + public boolean copyHeader() { + return copyHeader; + } + + /** + * Sets whether or not the header should be copied from a default source. + *

+ * If this is true, if a default {@link FileConfiguration} is passed to + * {@link + * FileConfiguration#setDefaults(Configuration)} + * then upon saving it will use the header from that config, instead of + * the one provided here. + *

+ * If no default is set on the configuration, or the default is not of + * type FileConfiguration, or that config has no header ({@link #header()} + * returns null) then the header specified in this configuration will be + * used. + *

+ * Defaults to true. + * + * @param value Whether or not to copy the header + * @return This object, for chaining + */ + public FileConfigurationOptions copyHeader(boolean value) { + copyHeader = value; + + return this; + } +} diff --git a/src/main/java/com/georgev22/api/yaml/file/YamlConfiguration.java b/src/main/java/com/georgev22/api/yaml/file/YamlConfiguration.java new file mode 100644 index 0000000..50af332 --- /dev/null +++ b/src/main/java/com/georgev22/api/yaml/file/YamlConfiguration.java @@ -0,0 +1,212 @@ +package com.georgev22.api.yaml.file; + +import java.io.File; +import java.io.FileNotFoundException; +import java.io.IOException; +import java.io.Reader; +import java.util.Map; +import java.util.logging.Level; +import java.util.logging.Logger; + +import com.georgev22.api.utilities.Utils; +import org.jetbrains.annotations.NotNull; +import org.yaml.snakeyaml.DumperOptions; +import org.yaml.snakeyaml.Yaml; +import org.yaml.snakeyaml.error.YAMLException; +import org.yaml.snakeyaml.representer.Representer; +import com.georgev22.api.yaml.Configuration; +import com.georgev22.api.yaml.ConfigurationSection; +import com.georgev22.api.yaml.InvalidConfigurationException; + +/** + * An implementation of {@link Configuration} which saves all files in Yaml. + * Note that this implementation is not synchronized. + */ +public class YamlConfiguration extends FileConfiguration { + protected static final String COMMENT_PREFIX = "# "; + protected static final String BLANK_CONFIG = "{}\n"; + private final DumperOptions yamlOptions = new DumperOptions(); + private final Representer yamlRepresenter = new YamlRepresenter(); + private final Yaml yaml = new Yaml(new YamlConstructor(), yamlRepresenter, yamlOptions); + + /** + * Creates a new {@link YamlConfiguration}, loading from the given file. + *

+ * Any errors loading the Configuration will be logged and then ignored. + * If the specified input is not a valid config, a blank config will be + * returned. + *

+ * The encoding used may follow the system dependent default. + * + * @param file Input file + * @return Resulting configuration + * @throws IllegalArgumentException Thrown if file is null + */ + public static YamlConfiguration loadConfiguration(File file) { + Utils.Assertions.notNull("File cannot be null", file); + + YamlConfiguration config = new YamlConfiguration(); + + try { + config.load(file); + } catch (FileNotFoundException ex) { + } catch (IOException | InvalidConfigurationException ex) { + Logger.getLogger("").log(Level.SEVERE, "Cannot load " + file, ex); + } + + return config; + } + + /** + * Creates a new {@link YamlConfiguration}, loading from the given reader. + *

+ * Any errors loading the Configuration will be logged and then ignored. + * If the specified input is not a valid config, a blank config will be + * returned. + * + * @param reader input + * @return resulting configuration + * @throws IllegalArgumentException Thrown if stream is null + */ + public static @NotNull YamlConfiguration loadConfiguration(Reader reader) { + Utils.Assertions.notNull("Stream cannot be null", reader); + + YamlConfiguration config = new YamlConfiguration(); + + try { + config.load(reader); + } catch (IOException | InvalidConfigurationException ex) { + Logger.getLogger("").log(Level.SEVERE, "Cannot load configuration from stream", ex); + } + + return config; + } + + @Override + public String saveToString() { + yamlOptions.setIndent(options().indent()); + yamlOptions.setDefaultFlowStyle(DumperOptions.FlowStyle.BLOCK); + yamlRepresenter.setDefaultFlowStyle(DumperOptions.FlowStyle.BLOCK); + + String header = buildHeader(); + String dump = yaml.dump(getValues(false)); + + if (dump.equals(BLANK_CONFIG)) { + dump = ""; + } + + return header + dump; + } + + @Override + public void loadFromString(String contents) throws InvalidConfigurationException { + Utils.Assertions.notNull("Contents cannot be null", contents); + + Map input; + try { + input = (Map) yaml.load(contents); + } catch (YAMLException e) { + throw new InvalidConfigurationException(e); + } catch (ClassCastException e) { + throw new InvalidConfigurationException("Top level is not a Map."); + } + + String header = parseHeader(contents); + if (header.length() > 0) { + options().header(header); + } + + if (input != null) { + convertMapsToSections(input, this); + } + } + + protected void convertMapsToSections(Map input, ConfigurationSection section) { + for (Map.Entry entry : input.entrySet()) { + String key = entry.getKey().toString(); + Object value = entry.getValue(); + + if (value instanceof Map) { + convertMapsToSections((Map) value, section.createSection(key)); + } else { + section.set(key, value); + } + } + } + + protected String parseHeader(String input) { + String[] lines = input.split("\r?\n", -1); + StringBuilder result = new StringBuilder(); + boolean readingHeader = true; + boolean foundHeader = false; + + for (int i = 0; (i < lines.length) && (readingHeader); i++) { + String line = lines[i]; + + if (line.startsWith(COMMENT_PREFIX)) { + if (i > 0) { + result.append("\n"); + } + + if (line.length() > COMMENT_PREFIX.length()) { + result.append(line.substring(COMMENT_PREFIX.length())); + } + + foundHeader = true; + } else if ((foundHeader) && (line.length() == 0)) { + result.append("\n"); + } else if (foundHeader) { + readingHeader = false; + } + } + + return result.toString(); + } + + @Override + protected String buildHeader() { + String header = options().header(); + + if (options().copyHeader()) { + Configuration def = getDefaults(); + + if ((def != null) && (def instanceof FileConfiguration)) { + FileConfiguration filedefaults = (FileConfiguration) def; + String defaultsHeader = filedefaults.buildHeader(); + + if ((defaultsHeader != null) && (defaultsHeader.length() > 0)) { + return defaultsHeader; + } + } + } + + if (header == null) { + return ""; + } + + StringBuilder builder = new StringBuilder(); + String[] lines = header.split("\r?\n", -1); + boolean startedHeader = false; + + for (int i = lines.length - 1; i >= 0; i--) { + builder.insert(0, "\n"); + + if ((startedHeader) || (lines[i].length() != 0)) { + builder.insert(0, lines[i]); + builder.insert(0, COMMENT_PREFIX); + startedHeader = true; + } + } + + return builder.toString(); + } + + @Override + public YamlConfigurationOptions options() { + if (options == null) { + options = new YamlConfigurationOptions(this); + } + + return (YamlConfigurationOptions) options; + } +} diff --git a/src/main/java/com/georgev22/api/yaml/file/YamlConfigurationOptions.java b/src/main/java/com/georgev22/api/yaml/file/YamlConfigurationOptions.java new file mode 100644 index 0000000..a52f02e --- /dev/null +++ b/src/main/java/com/georgev22/api/yaml/file/YamlConfigurationOptions.java @@ -0,0 +1,71 @@ +package com.georgev22.api.yaml.file; + +import com.georgev22.api.utilities.Utils; + +/** + * Various settings for controlling the input and output of a {@link + * YamlConfiguration} + */ +public class YamlConfigurationOptions extends FileConfigurationOptions { + private int indent = 2; + + protected YamlConfigurationOptions(YamlConfiguration configuration) { + super(configuration); + } + + @Override + public YamlConfiguration configuration() { + return (YamlConfiguration) super.configuration(); + } + + @Override + public YamlConfigurationOptions copyDefaults(boolean value) { + super.copyDefaults(value); + return this; + } + + @Override + public YamlConfigurationOptions pathSeparator(char value) { + super.pathSeparator(value); + return this; + } + + @Override + public YamlConfigurationOptions header(String value) { + super.header(value); + return this; + } + + @Override + public YamlConfigurationOptions copyHeader(boolean value) { + super.copyHeader(value); + return this; + } + + /** + * Gets how many spaces should be used to indent each line. + *

+ * The minimum value this may be is 2, and the maximum is 9. + * + * @return How much to indent by + */ + public int indent() { + return indent; + } + + /** + * Sets how many spaces should be used to indent each line. + *

+ * The minimum value this may be is 2, and the maximum is 9. + * + * @param value New indent + * @return This object, for chaining + */ + public YamlConfigurationOptions indent(int value) { + Utils.Assertions.isTrue("Indent must be at least 2 characters", value >= 2); + Utils.Assertions.isTrue("Indent cannot be greater than 9 characters", value <= 9); + + this.indent = value; + return this; + } +} diff --git a/src/main/java/com/georgev22/api/yaml/file/YamlConstructor.java b/src/main/java/com/georgev22/api/yaml/file/YamlConstructor.java new file mode 100644 index 0000000..f7c5bb5 --- /dev/null +++ b/src/main/java/com/georgev22/api/yaml/file/YamlConstructor.java @@ -0,0 +1,47 @@ +package com.georgev22.api.yaml.file; + +import com.georgev22.api.yaml.serialization.ConfigurationSerialization; +import java.util.LinkedHashMap; +import java.util.Map; +import org.yaml.snakeyaml.constructor.SafeConstructor; +import org.yaml.snakeyaml.error.YAMLException; +import org.yaml.snakeyaml.nodes.Node; +import org.yaml.snakeyaml.nodes.Tag; + +public class YamlConstructor extends SafeConstructor { + + public YamlConstructor() { + this.yamlConstructors.put(Tag.MAP, new ConstructCustomObject()); + } + + private class ConstructCustomObject extends ConstructYamlMap { + @Override + public Object construct(Node node) { + if (node.isTwoStepsConstruction()) { + throw new YAMLException("Unexpected referential mapping structure. Node: " + node); + } + + Map raw = (Map) super.construct(node); + + if (raw.containsKey(ConfigurationSerialization.SERIALIZED_TYPE_KEY)) { + Map typed = new LinkedHashMap<>(raw.size()); + for (Map.Entry entry : raw.entrySet()) { + typed.put(entry.getKey().toString(), entry.getValue()); + } + + try { + return ConfigurationSerialization.deserializeObject(typed); + } catch (IllegalArgumentException ex) { + throw new YAMLException("Could not deserialize object", ex); + } + } + + return raw; + } + + @Override + public void construct2ndStep(Node node, Object object) { + throw new YAMLException("Unexpected referential mapping structure. Node: " + node); + } + } +} diff --git a/src/main/java/com/georgev22/api/yaml/file/YamlRepresenter.java b/src/main/java/com/georgev22/api/yaml/file/YamlRepresenter.java new file mode 100644 index 0000000..2969f3e --- /dev/null +++ b/src/main/java/com/georgev22/api/yaml/file/YamlRepresenter.java @@ -0,0 +1,36 @@ +package com.georgev22.api.yaml.file; + +import com.georgev22.api.yaml.ConfigurationSection; +import com.georgev22.api.yaml.serialization.ConfigurationSerializable; +import com.georgev22.api.yaml.serialization.ConfigurationSerialization; +import java.util.LinkedHashMap; +import java.util.Map; +import org.yaml.snakeyaml.nodes.Node; +import org.yaml.snakeyaml.representer.Representer; + +public class YamlRepresenter extends Representer { + + public YamlRepresenter() { + this.multiRepresenters.put(ConfigurationSection.class, new RepresentConfigurationSection()); + this.multiRepresenters.put(ConfigurationSerializable.class, new RepresentConfigurationSerializable()); + } + + private class RepresentConfigurationSection extends RepresentMap { + @Override + public Node representData(Object data) { + return super.representData(((ConfigurationSection) data).getValues(false)); + } + } + + private class RepresentConfigurationSerializable extends RepresentMap { + @Override + public Node representData(Object data) { + ConfigurationSerializable serializable = (ConfigurationSerializable) data; + Map values = new LinkedHashMap<>(); + values.put(ConfigurationSerialization.SERIALIZED_TYPE_KEY, ConfigurationSerialization.getAlias(serializable.getClass())); + values.putAll(serializable.serialize()); + + return super.representData(values); + } + } +} diff --git a/src/main/java/com/georgev22/api/yaml/serialization/ConfigurationSerializable.java b/src/main/java/com/georgev22/api/yaml/serialization/ConfigurationSerializable.java new file mode 100644 index 0000000..c790e06 --- /dev/null +++ b/src/main/java/com/georgev22/api/yaml/serialization/ConfigurationSerializable.java @@ -0,0 +1,35 @@ +package com.georgev22.api.yaml.serialization; + +import java.util.Map; + +/** + * Represents an object that may be serialized. + *

+ * These objects MUST implement one of the following, in addition to the + * methods as defined by this interface: + *

+ * In addition to implementing this interface, you must register the class + * with {@link ConfigurationSerialization#registerClass(Class)}. + * + * @see DelegateDeserialization + * @see SerializableAs + */ +public interface ConfigurationSerializable { + + /** + * Creates a Map representation of this class. + *

+ * This class must provide a method to restore this class, as defined in + * the {@link ConfigurationSerializable} interface javadocs. + * + * @return Map containing the current state of this class + */ + public Map serialize(); +} diff --git a/src/main/java/com/georgev22/api/yaml/serialization/ConfigurationSerialization.java b/src/main/java/com/georgev22/api/yaml/serialization/ConfigurationSerialization.java new file mode 100644 index 0000000..2ec95e3 --- /dev/null +++ b/src/main/java/com/georgev22/api/yaml/serialization/ConfigurationSerialization.java @@ -0,0 +1,263 @@ +package com.georgev22.api.yaml.serialization; + +import com.georgev22.api.utilities.Utils; +import com.georgev22.api.yaml.Configuration; + +import java.lang.reflect.Constructor; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.lang.reflect.Modifier; +import java.util.HashMap; +import java.util.Map; +import java.util.logging.Level; +import java.util.logging.Logger; + +/** + * Utility class for storing and retrieving classes for {@link Configuration}. + */ +public class ConfigurationSerialization { + public static final String SERIALIZED_TYPE_KEY = "=="; + private static Map> aliases = new HashMap<>(); + + private final Class clazz; + + protected ConfigurationSerialization(Class clazz) { + this.clazz = clazz; + } + + /** + * Attempts to deserialize the given arguments into a new instance of the + * given class. + *

+ * The class must implement {@link ConfigurationSerializable}, including + * the extra methods as specified in the javadoc of + * ConfigurationSerializable. + *

+ * If a new instance could not be made, an example being the class not + * fully implementing the interface, null will be returned. + * + * @param args Arguments for deserialization + * @param clazz Class to deserialize into + * @return New instance of the specified class + */ + public static ConfigurationSerializable deserializeObject(Map args, Class clazz) { + return new ConfigurationSerialization(clazz).deserialize(args); + } + + /** + * Attempts to deserialize the given arguments into a new instance of the + * given class. + *

+ * The class must implement {@link ConfigurationSerializable}, including + * the extra methods as specified in the javadoc of + * ConfigurationSerializable. + *

+ * If a new instance could not be made, an example being the class not + * fully implementing the interface, null will be returned. + * + * @param args Arguments for deserialization + * @return New instance of the specified class + */ + public static ConfigurationSerializable deserializeObject(Map args) { + Class clazz = null; + + if (args.containsKey(SERIALIZED_TYPE_KEY)) { + try { + String alias = (String) args.get(SERIALIZED_TYPE_KEY); + + if (alias == null) { + throw new IllegalArgumentException("Cannot have null alias"); + } + clazz = getClassByAlias(alias); + if (clazz == null) { + throw new IllegalArgumentException("Specified class does not exist ('" + alias + "')"); + } + } catch (ClassCastException ex) { + ex.fillInStackTrace(); + throw ex; + } + } else { + throw new IllegalArgumentException("Args doesn't contain type key ('" + SERIALIZED_TYPE_KEY + "')"); + } + + return new ConfigurationSerialization(clazz).deserialize(args); + } + + /** + * Registers the given {@link ConfigurationSerializable} class by its + * alias + * + * @param clazz Class to register + */ + public static void registerClass(Class clazz) { + DelegateDeserialization delegate = clazz.getAnnotation(DelegateDeserialization.class); + + if (delegate == null) { + registerClass(clazz, getAlias(clazz)); + registerClass(clazz, clazz.getName()); + } + } + + /** + * Registers the given alias to the specified {@link + * ConfigurationSerializable} class + * + * @param clazz Class to register + * @param alias Alias to register as + * @see SerializableAs + */ + public static void registerClass(Class clazz, String alias) { + aliases.put(alias, clazz); + } + + /** + * Unregisters the specified alias to a {@link ConfigurationSerializable} + * + * @param alias Alias to unregister + */ + public static void unregisterClass(String alias) { + aliases.remove(alias); + } + + /** + * Unregisters any aliases for the specified {@link + * ConfigurationSerializable} class + * + * @param clazz Class to unregister + */ + public static void unregisterClass(Class clazz) { + while (aliases.values().remove(clazz)) { + ; + } + } + + /** + * Attempts to get a registered {@link ConfigurationSerializable} class by + * its alias + * + * @param alias Alias of the serializable + * @return Registered class, or null if not found + */ + public static Class getClassByAlias(String alias) { + return aliases.get(alias); + } + + /** + * Gets the correct alias for the given {@link ConfigurationSerializable} + * class + * + * @param clazz Class to get alias for + * @return Alias to use for the class + */ + public static String getAlias(Class clazz) { + DelegateDeserialization delegate = clazz.getAnnotation(DelegateDeserialization.class); + + if (delegate != null) { + if ((delegate.value() == null) || (delegate.value() == clazz)) { + delegate = null; + } else { + return getAlias(delegate.value()); + } + } + + if (delegate == null) { + SerializableAs alias = clazz.getAnnotation(SerializableAs.class); + + if ((alias != null) && (alias.value() != null)) { + return alias.value(); + } + } + + return clazz.getName(); + } + + protected Method getMethod(String name, boolean isStatic) { + try { + Method method = clazz.getDeclaredMethod(name, Map.class); + + if (!ConfigurationSerializable.class.isAssignableFrom(method.getReturnType())) { + return null; + } + if (Modifier.isStatic(method.getModifiers()) != isStatic) { + return null; + } + + return method; + } catch (NoSuchMethodException | SecurityException ex) { + return null; + } + } + + protected Constructor getConstructor() { + try { + return clazz.getConstructor(Map.class); + } catch (NoSuchMethodException | SecurityException ex) { + return null; + } + } + + protected ConfigurationSerializable deserializeViaMethod(Method method, Map args) { + try { + ConfigurationSerializable result = (ConfigurationSerializable) method.invoke(null, args); + + if (result == null) { + Logger.getLogger(ConfigurationSerialization.class.getName()).log(Level.SEVERE, "Could not call method '" + method.toString() + "' of " + clazz + " for deserialization: method returned null"); + } else { + return result; + } + } catch (Throwable ex) { + Logger.getLogger(ConfigurationSerialization.class.getName()).log( + Level.SEVERE, + "Could not call method '" + method.toString() + "' of " + clazz + " for deserialization", + ex instanceof InvocationTargetException ? ex.getCause() : ex); + } + + return null; + } + + protected ConfigurationSerializable deserializeViaCtor(Constructor ctor, Map args) { + try { + return ctor.newInstance(args); + } catch (Throwable ex) { + Logger.getLogger(ConfigurationSerialization.class.getName()).log( + Level.SEVERE, + "Could not call constructor '" + ctor.toString() + "' of " + clazz + " for deserialization", + ex instanceof InvocationTargetException ? ex.getCause() : ex); + } + + return null; + } + + public ConfigurationSerializable deserialize(Map args) { + Utils.Assertions.notNull("Args must not be null", args); + + ConfigurationSerializable result = null; + Method method = null; + + if (result == null) { + method = getMethod("deserialize", true); + + if (method != null) { + result = deserializeViaMethod(method, args); + } + } + + if (result == null) { + method = getMethod("valueOf", true); + + if (method != null) { + result = deserializeViaMethod(method, args); + } + } + + if (result == null) { + Constructor constructor = getConstructor(); + + if (constructor != null) { + result = deserializeViaCtor(constructor, args); + } + } + + return result; + } +} diff --git a/src/main/java/com/georgev22/api/yaml/serialization/DelegateDeserialization.java b/src/main/java/com/georgev22/api/yaml/serialization/DelegateDeserialization.java new file mode 100644 index 0000000..3b15592 --- /dev/null +++ b/src/main/java/com/georgev22/api/yaml/serialization/DelegateDeserialization.java @@ -0,0 +1,22 @@ +package com.georgev22.api.yaml.serialization; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * Applies to a {@link ConfigurationSerializable} that will delegate all + * deserialization to another {@link ConfigurationSerializable}. + */ +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.TYPE) +public @interface DelegateDeserialization { + /** + * Which class should be used as a delegate for this classes + * deserialization + * + * @return Delegate class + */ + public Class value(); +} diff --git a/src/main/java/com/georgev22/api/yaml/serialization/SerializableAs.java b/src/main/java/com/georgev22/api/yaml/serialization/SerializableAs.java new file mode 100644 index 0000000..2979342 --- /dev/null +++ b/src/main/java/com/georgev22/api/yaml/serialization/SerializableAs.java @@ -0,0 +1,34 @@ +package com.georgev22.api.yaml.serialization; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * Represents an "alias" that a {@link ConfigurationSerializable} may be + * stored as. + * If this is not present on a {@link ConfigurationSerializable} class, it + * will use the fully qualified name of the class. + *

+ * This value will be stored in the configuration so that the configuration + * deserialization can determine what type it is. + *

+ * Using this annotation on any other class than a {@link + * ConfigurationSerializable} will have no effect. + * + * @see ConfigurationSerialization#registerClass(Class, String) + */ +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.TYPE) +public @interface SerializableAs { + /** + * This is the name your class will be stored and retrieved as. + *

+ * This name MUST be unique. We recommend using names such as + * "MyPluginThing" instead of "Thing". + * + * @return Name to serialize the class as. + */ + public String value(); +}