/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
package pluginbase.bukkit.properties;
import pluginbase.logging.PluginLogger;
import pluginbase.properties.AbstractProperties;
import pluginbase.properties.ListProperty;
import pluginbase.properties.MappedProperty;
import pluginbase.properties.NestedProperties;
import pluginbase.properties.NestedProperty;
import pluginbase.properties.Null;
import pluginbase.properties.Properties;
import pluginbase.properties.Property;
import pluginbase.properties.SimpleProperty;
import pluginbase.properties.ValueProperty;
import pluginbase.properties.serializers.DefaultSerializer;
import pluginbase.properties.serializers.DefaultStringSerializer;
import pluginbase.properties.serializers.StringStringSerializer;
import org.bukkit.configuration.ConfigurationOptions;
import org.bukkit.configuration.ConfigurationSection;
import org.bukkit.configuration.file.FileConfiguration;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
/**
* A Properties implementation that stores Property values into a {@link FileConfiguration}.
*/
public abstract class AbstractFileProperties extends AbstractProperties implements Properties {
/** The file configuration object that backs this Properties object. */
@NotNull
protected final FileConfiguration config;
private final Map<NestedProperty, NestedFileProperties> nestMap = new HashMap<NestedProperty, NestedFileProperties>();
/**
* Constructs a new Properties object using the given config to store Property values in.
*
* @param logger a logger to use for any important messages this Properties object may need to log.
* @param config the persistence object.
* @param configClasses the classes that declare what {@link Property} objects belong to this Properties object.
*/
protected AbstractFileProperties(@NotNull final PluginLogger logger,
@NotNull final FileConfiguration config,
@NotNull final Class... configClasses) {
super(logger, configClasses);
this.config = config;
for (final Property property : getProperties()) {
if (property instanceof ValueProperty) {
final ValueProperty valueProperty = (ValueProperty) property;
final Class type = valueProperty.getType();
if (valueProperty.getDefaultSerializer() != null) {
setPropertySerializer(type, valueProperty.getDefaultSerializer());
} else if (!hasPropertySerializer(type)) {
if (type.equals(String.class)) {
setPropertySerializer(type, new StringStringSerializer(type));
} else {
try {
setPropertySerializer(type, new DefaultStringSerializer(type));
} catch (IllegalArgumentException e) {
setPropertySerializer(type, new DefaultSerializer(type));
}
}
}
}
}
}
/**
* Gets the config object backing this Properties object.
* <p/>
* Should be overridden in NestedProperties to give the appropriate ConfigurationSection for the Properties sub
* path of the backing Configuration object.
*
* @return the config object backing this Properties object.
*/
@NotNull
protected ConfigurationSection getConfig() {
return this.config;
}
/**
* Gets the configuration options for the backing file configuration.
*
* @return the configuration options for the backing file configuration.
*/
@NotNull
protected final ConfigurationOptions getConfigOptions() {
return this.config.options();
}
/**
* The name of this Properties object which is used to define the path it holds in the Configuration object.
* <p/>
* Should be overridden in NestedProperties to indicate the sub path this Properties object has in the
* backing Configuration object.
*
* @return the name of this Properties object which is used to define the path it holds in the Configuration object.
*/
@NotNull
protected String getName() {
return "";
}
/**
* Tells this AbstractFileProperties to associate the comments for its Property objects with the key associated with
* the Property's value in the backing Configuration object.
* <p/>
* This is typically done right before saving the Configuration to disk.
*
* @param config The backing configuration object to apply comments to.
*/
protected final void doComments(@NotNull final CommentedFile config) {
for (final Property property : getProperties()) {
final String path;
if (!getName().isEmpty()) {
path = getName() + getConfigOptions().pathSeparator() + property.getName();
} else {
path = property.getName();
}
config.addComments(path, property.getComments());
if (property instanceof NestedProperty) {
NestedFileProperties nestedProperties = this.nestMap.get(property);
if (nestedProperties != null) {
nestedProperties.doComments(config);
}
}
}
}
/**
* Deserializes all the values in the backing Configuration object.
* <p/>
* This causes the Configuration object to hold references to the actual objects each Property represents instead
* of references to serialized form.
* <p/>
* If this is overridden this super method should probably still be called.
*/
protected void deserializeAll() {
for (final Property property : getProperties()) {
if (property instanceof ValueProperty) {
final ValueProperty valueProperty = (ValueProperty) property;
if (getConfig().get(valueProperty.getName()) != null) {
if (valueProperty instanceof MappedProperty) {
ConfigurationSection section = getConfig().getConfigurationSection(valueProperty.getName());
if (section == null) {
getConfig().set(valueProperty.getName(), valueProperty.getDefault());
} else {
for (String key : section.getKeys(false)) {
final Object obj = section.get(key);
if (obj != null) {
final Object res = getPropertySerializer(valueProperty.getType()).deserialize(obj);
if (isValid(valueProperty, res)) {
section.set(key, res);
} else {
getLog().warning("Invalid value '" + obj + "' at '" + valueProperty.getName() + getConfigOptions().pathSeparator() + key + "'. Value will be deleted!");
section.set(key, null);
}
}
}
}
} else if (valueProperty instanceof ListProperty) {
List list = getConfig().getList(valueProperty.getName());
if (list == null) {
getConfig().set(valueProperty.getName(), valueProperty.getDefault());
} else {
List newList = ((ListProperty) valueProperty).getNewTypeList();
for (int i = 0; i < list.size(); i++) {
final Object res = getPropertySerializer(valueProperty.getType()).deserialize(list.get(i));
if (isValid(valueProperty, res)) {
newList.add(res);
} else {
getLog().warning("Invalid value '" + res + "' at '" + valueProperty.getName() + "[" + i + "]'. Value will be deleted!");
}
}
getConfig().set(valueProperty.getName(), newList);
}
} else if (valueProperty instanceof SimpleProperty && !valueProperty.getType().isAssignableFrom(Null.class)) {
Object obj = getConfig().get(valueProperty.getName());
if (obj == null) {
getConfig().set(valueProperty.getName(), valueProperty.getDefault());
} else {
Object res = getPropertySerializer(valueProperty.getType()).deserialize(obj);
if (isValid(valueProperty, res)) {
getConfig().set(valueProperty.getName(), res);
} else {
getLog().warning("Invalid value '" + obj + "' at '" + valueProperty.getName() + "'. Value will be defaulted!");
getConfig().set(valueProperty.getName(), valueProperty.getDefault());
}
}
}
}
} else if (property instanceof NestedProperty) {
final NestedProperty nestedProperty = (NestedProperty) property;
final NestedFileProperties nestedProperties = new NestedFileProperties(getLog(), config, this,
nestedProperty.getName(), nestedProperty.getType());
nestedProperties.deserializeAll();
getConfig().set(nestedProperty.getName(), nestedProperties.getConfig());
this.nestMap.put(nestedProperty, nestedProperties);
}
}
}
/**
* Serializes all the values in the backing Configuration object.
* <p/>
* This causes the Configuration object to hold references to the serialized form of the objects each Property
* represents instead of references to the actual form.
* <p/>
* If this is overridden this super method should probably still be called.
*
* @param newConfig the section of the config to be serializing for.
*/
protected void serializeAll(@NotNull final ConfigurationSection newConfig) {
for (final Property property : getProperties()) {
if (property instanceof ValueProperty) {
final ValueProperty valueProperty = (ValueProperty) property;
if (getConfig().get(valueProperty.getName()) != null) {
if (valueProperty instanceof MappedProperty) {
Object o = getConfig().get(valueProperty.getName());
if (o == null) {
getLog().fine("Missing property: %s", valueProperty.getName());
continue;
}
Map map;
if (o instanceof ConfigurationSection) {
map = ((ConfigurationSection) o).getValues(false);
} else if (!(o instanceof Map)) {
getLog().fine("Missing property: %s", valueProperty.getName());
continue;
} else {
map = (Map) o;
}
for (Object key : map.keySet()) {
Object obj = map.get(key);
if (valueProperty.getType().isInstance(obj)) {
if (obj != null) {
map.put(key, getPropertySerializer(valueProperty.getType()).serialize(valueProperty.getType().cast(obj)));
}
} else {
getLog().warning("Could not serialize: %s", valueProperty.getName());
}
}
newConfig.set(valueProperty.getName(), map);
} else if (valueProperty instanceof ListProperty) {
List list = getConfig().getList(valueProperty.getName());
if (list == null) {
getLog().fine("Missing property: %s", valueProperty.getName());
continue;
}
List newList = new ArrayList(list.size());
for (Object obj : list) {
if (valueProperty.getType().isInstance(obj)) {
if (obj != null) {
newList.add(getPropertySerializer(valueProperty.getType()).serialize(valueProperty.getType().cast(obj)));
}
} else {
getLog().warning("Could not serialize: %s", valueProperty.getName());
}
}
newConfig.set(valueProperty.getName(), newList);
} else if (valueProperty instanceof SimpleProperty && !valueProperty.getType().isAssignableFrom(Null.class)) {
Object obj = getConfig().get(valueProperty.getName());
if (obj == null) {
getLog().fine("Missing property: %s", valueProperty.getName());
continue;
}
if (valueProperty.getType().isInstance(obj)) {
Object res = getPropertySerializer(valueProperty.getType()).serialize(valueProperty.getType().cast(obj));
newConfig.set(valueProperty.getName(), res);
} else {
getLog().warning("Could not serialize '%s' since value is '%s' instead of '%s'", valueProperty.getName(), obj.getClass(), valueProperty.getType());
}
}
}
} else if (property instanceof NestedProperty) {
final NestedProperty nestedProperty = (NestedProperty) property;
final NestedFileProperties nestedProperties = this.nestMap.get(nestedProperty);
if (nestedProperties != null) {
nestedProperties.serializeAll(newConfig.createSection(nestedProperty.getName()));
} else {
//TODO Warn
}
}
}
}
/**
* Loads default settings for any missing config values.
* <p/>
* If this is overridden this super method should probably still be called.
*/
protected void setDefaults() {
for (Property path : getProperties()) {
if (path instanceof ValueProperty) {
ValueProperty valueProperty = (ValueProperty) path;
if (getConfig().get(valueProperty.getName()) == null) {
if (valueProperty.isDeprecated()) {
continue;
}
if (valueProperty instanceof MappedProperty) {
getLog().fine("Config: Defaulting map for '%s'", valueProperty.getName());
if (valueProperty.getDefault() != null) {
getConfig().set(valueProperty.getName(), valueProperty.getDefault());
} else {
getConfig().set(valueProperty.getName(), ((MappedProperty) valueProperty).getNewTypeMap());
}
} else if (valueProperty instanceof ListProperty) {
ListProperty listPath = (ListProperty) valueProperty;
getLog().fine("Config: Defaulting list for '%s'", valueProperty.getName());
if (listPath.getDefault() != null) {
getConfig().set(valueProperty.getName(), listPath.getDefault());
} else {
getConfig().set(valueProperty.getName(), listPath.getNewTypeList());
}
} else if (valueProperty.getDefault() != null) {
getLog().fine("Config: Defaulting '%s' to %s", valueProperty.getName(), valueProperty.getDefault());
getConfig().set(valueProperty.getName(), valueProperty.getDefault());
}
}
} else if (path instanceof NestedProperty) {
final NestedProperty nestedProperty = (NestedProperty) path;
final NestedFileProperties nestedProperties = this.nestMap.get(nestedProperty);
if (nestedProperties != null) {
nestedProperties.setDefaults();
} else {
//TODO Warn
}
}
}
}
@Nullable
private Object getEntryValue(@NotNull final ValueProperty valueProperty) throws IllegalArgumentException {
if (!isInConfig(valueProperty)) {
throw new IllegalArgumentException("property not registered to this config!");
}
Object obj = getConfig().get(valueProperty.getName());
if (obj == null) {
if (valueProperty.shouldDefaultIfMissing()) {
obj = valueProperty.getDefault();
}
}
return obj;
}
/** {@inheritDoc} */
@Nullable
@Override
public <T> T get(@NotNull final SimpleProperty<T> entry) throws IllegalArgumentException {
Object obj = null;
try {
obj = getEntryValue(entry);
} catch (IllegalArgumentException e) {
throw (IllegalArgumentException) e.fillInStackTrace();
}
if (obj == null) {
return null;
}
if (!entry.getType().isInstance(obj)) {
getLog().fine("An invalid value of '%s' was detected at '%s' during a get call. Attempting to deserialize and replace...", obj, entry.getName());
final Object res = getPropertySerializer(entry.getType()).deserialize(obj);
if (isValid(entry, (T) res)) {
obj = res;
}
}
return entry.getType().cast(obj);
}
/** {@inheritDoc} */
@Nullable
@Override
public <T> List<T> get(@NotNull final ListProperty<T> entry) {
Object obj = null;
try {
obj = getEntryValue(entry);
} catch (IllegalArgumentException e) {
throw (IllegalArgumentException) e.fillInStackTrace();
}
if (obj == null) {
return null;
}
if (!(obj instanceof List)) {
obj = new ArrayList<Object>();
}
List list = (List) obj;
for (int i = 0; i < list.size(); i++) {
Object o = list.get(i);
if (!entry.getType().isInstance(o)) {
getLog().fine("An invalid value of '%s' was detected at '%s[%s]' during a get call. Attempting to deserialize and replace...", o, entry.getName(), i);
final Object res = getPropertySerializer(entry.getType()).deserialize(o);
if (isValid(entry, (T) res)) {
o = res;
list.set(i, entry.getType().cast(o));
} else {
getLog().warning("Invalid value '%s' at '%s[%s]'!", obj, entry.getName(), i);
continue;
}
}
}
return list;
}
/** {@inheritDoc} */
@Nullable
@Override
public <T> Map<String, T> get(@NotNull final MappedProperty<T> entry) {
Object obj = null;
try {
obj = getEntryValue(entry);
} catch (IllegalArgumentException e) {
throw (IllegalArgumentException) e.fillInStackTrace();
}
if (obj == null) {
return null;
}
if (obj instanceof ConfigurationSection) {
obj = ((ConfigurationSection) obj).getValues(false);
}
if (!(obj instanceof Map)) {
obj = new HashMap<String, Object>();
}
Map<String, Object> map = (Map<String, Object>) obj;
for (Map.Entry<String, Object> mapEntry : map.entrySet()) {
Object o = mapEntry.getValue();
if (!entry.getType().isInstance(o)) {
getLog().fine("An invalid value of '%s' was detected at '%s%s%s' during a get call. Attempting to deserialize and replace...", o, entry.getName(), getConfigOptions().pathSeparator(), mapEntry.getKey());
final Object res = getPropertySerializer(entry.getType()).deserialize(o);
if (isValid(entry, (T) res)) {
o = res;
map.put(mapEntry.getKey(), entry.getType().cast(o));
} else {
getLog().warning("Invalid value '%s' at '%s%s%s'!", obj, entry.getName() + getConfigOptions().pathSeparator() + mapEntry.getKey());
continue;
}
}
}
return (Map<String, T>) map;
}
/** {@inheritDoc} */
@Nullable
@Override
public <T> T get(@NotNull final MappedProperty<T> entry, @NotNull final String key) {
if (!isInConfig(entry)) {
throw new IllegalArgumentException("entry not registered to this config!");
}
final String path = entry.getName() + getConfigOptions().pathSeparator() + key;
Object obj = getConfig().get(path);
if (obj == null) {
return null;
}
if (!entry.getType().isInstance(obj)) {
getLog().fine("An invalid value of '%s' was detected at '%s' during a get call. Attempting to deserialize and replace...", obj, path);
final Object res = getPropertySerializer(entry.getType()).deserialize(obj);
if (isValid(entry, (T) res)) {
obj = res;
}
}
return entry.getType().cast(obj);
}
/** {@inheritDoc} */
@NotNull
@Override
public NestedProperties get(@NotNull final NestedProperty entry) {
if (!isInConfig(entry)) {
throw new IllegalArgumentException("entry not registered to this config!");
}
return this.nestMap.get(entry);
}
/** {@inheritDoc} */
@Override
public <T> boolean set(@NotNull final SimpleProperty<T> entry, @Nullable final T value) {
if (!isInConfig(entry)) {
throw new IllegalArgumentException("Property not registered to this config!");
}
if (value == null) {
getConfig().set(entry.getName(), null);
changed(entry);
return true;
}
if (!isValid(entry, value)) {
return false;
}
getConfig().set(entry.getName(), value);
changed(entry);
return true;
}
/** {@inheritDoc} */
@Override
public <T> boolean set(@NotNull final ListProperty<T> entry, @Nullable final List<T> newValue) {
if (!isInConfig(entry)) {
throw new IllegalArgumentException("Property not registered to this config!");
}
getConfig().set(entry.getName(), newValue);
changed(entry);
return true;
}
/** {@inheritDoc} */
@Override
public <T> boolean set(@NotNull final MappedProperty<T> entry, @Nullable final Map<String, T> newValue) {
if (!isInConfig(entry)) {
throw new IllegalArgumentException("Property not registered to this config!");
}
getConfig().set(entry.getName(), newValue);
changed(entry);
return true;
}
/** {@inheritDoc} */
@Override
public <T> boolean set(@NotNull final MappedProperty<T> entry, @NotNull final String key, @Nullable final T value) {
if (!isInConfig(entry)) {
throw new IllegalArgumentException("Property not registered to this config!");
}
if (!isValid(entry, value)) {
return false;
}
getConfig().set(entry.getName() + getConfigOptions().pathSeparator() + key, value);
changed(entry);
return true;
}
}