Essentials/Essentials/src/main/java/com/earth2me/essentials/config/EssentialsConfiguration.java

481 lines
17 KiB
Java

package com.earth2me.essentials.config;
import com.earth2me.essentials.Essentials;
import com.earth2me.essentials.config.annotations.DeleteIfIncomplete;
import com.earth2me.essentials.config.annotations.DeleteOnEmpty;
import com.earth2me.essentials.config.entities.CommandCooldown;
import com.earth2me.essentials.config.entities.LazyLocation;
import com.earth2me.essentials.config.processors.DeleteIfIncompleteProcessor;
import com.earth2me.essentials.config.processors.DeleteOnEmptyProcessor;
import com.earth2me.essentials.config.serializers.BigDecimalTypeSerializer;
import com.earth2me.essentials.config.serializers.CommandCooldownSerializer;
import com.earth2me.essentials.config.serializers.LocationTypeSerializer;
import com.earth2me.essentials.config.serializers.MailMessageSerializer;
import com.earth2me.essentials.config.serializers.MaterialTypeSerializer;
import net.ess3.api.InvalidWorldException;
import net.essentialsx.api.v2.services.mail.MailMessage;
import org.bukkit.Location;
import org.bukkit.Material;
import org.spongepowered.configurate.CommentedConfigurationNode;
import org.spongepowered.configurate.ConfigurateException;
import org.spongepowered.configurate.loader.HeaderMode;
import org.spongepowered.configurate.loader.ParsingException;
import org.spongepowered.configurate.objectmapping.ObjectMapper;
import org.spongepowered.configurate.serialize.SerializationException;
import org.spongepowered.configurate.serialize.TypeSerializerCollection;
import org.spongepowered.configurate.yaml.NodeStyle;
import org.spongepowered.configurate.yaml.YamlConfigurationLoader;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.lang.reflect.Type;
import java.math.BigDecimal;
import java.nio.file.Files;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.logging.Level;
import static com.earth2me.essentials.I18n.tl;
public class EssentialsConfiguration {
private static final ExecutorService EXECUTOR_SERVICE = Executors.newSingleThreadExecutor();
private static final ObjectMapper.Factory MAPPER_FACTORY = ObjectMapper.factoryBuilder()
.addProcessor(DeleteOnEmpty.class, (data, value) -> new DeleteOnEmptyProcessor())
.addProcessor(DeleteIfIncomplete.class, (data, value) -> new DeleteIfIncompleteProcessor())
.build();
private static final TypeSerializerCollection SERIALIZERS = TypeSerializerCollection.defaults().childBuilder()
.registerAnnotatedObjects(MAPPER_FACTORY)
.register(BigDecimal.class, new BigDecimalTypeSerializer())
.register(LazyLocation.class, new LocationTypeSerializer())
.register(Material.class, new MaterialTypeSerializer())
.register(CommandCooldown.class, new CommandCooldownSerializer())
.register(MailMessage.class, new MailMessageSerializer())
.build();
private final AtomicInteger pendingWrites = new AtomicInteger(0);
private final AtomicBoolean transaction = new AtomicBoolean(false);
private Class<?> resourceClass = EssentialsConfiguration.class;
protected final File configFile;
private final YamlConfigurationLoader loader;
private final String templateName;
private CommentedConfigurationNode configurationNode;
private Runnable saveHook;
public EssentialsConfiguration(final File configFile) {
this(configFile, null);
}
public EssentialsConfiguration(final File configFile, final String templateName) {
this(configFile, templateName, (String) null);
}
public EssentialsConfiguration(final File configFile, final String templateName, final Class<?> resourceClass) {
this(configFile, templateName, (String) null);
this.resourceClass = resourceClass;
}
public EssentialsConfiguration(final File configFile, final String templateName, final String header) {
this.configFile = configFile;
this.loader = YamlConfigurationLoader.builder()
.defaultOptions(opts -> opts
.header(header)
.serializers(SERIALIZERS))
.headerMode(HeaderMode.PRESET)
.nodeStyle(NodeStyle.BLOCK)
.indent(2)
.file(configFile)
.build();
this.templateName = templateName;
}
public CommentedConfigurationNode getRootNode() {
return configurationNode;
}
public void setRootHolder(final Class<?> holderClass, final Object holder) {
try {
getRootNode().set(holderClass, holder);
} catch (SerializationException e) {
Essentials.getWrappedLogger().log(Level.SEVERE, "Error while saving user config: " + configFile.getName(), e);
throw new RuntimeException(e);
}
}
public File getFile() {
return configFile;
}
public void setProperty(String path, final Location location) {
setInternal(path, LazyLocation.fromLocation(location));
}
public LazyLocation getLocation(final String path) throws InvalidWorldException {
final CommentedConfigurationNode node = path == null ? getRootNode() : getSection(path);
if (node == null) {
return null;
}
try {
return node.get(LazyLocation.class);
} catch (SerializationException e) {
return null;
}
}
public Map<String, LazyLocation> getLocationSectionMap(final String path) {
final CommentedConfigurationNode node = getSection(path);
final Map<String, LazyLocation> result = new HashMap<>();
for (final Map.Entry<String, CommentedConfigurationNode> entry : ConfigurateUtil.getMap(node).entrySet()) {
final CommentedConfigurationNode jailNode = entry.getValue();
try {
result.put(entry.getKey().toLowerCase(Locale.ENGLISH), jailNode.get(LazyLocation.class));
} catch (SerializationException e) {
Essentials.getWrappedLogger().log(Level.WARNING, "Error serializing key " + entry.getKey(), e);
}
}
return result;
}
public void setProperty(final String path, final List<?> list) {
setInternal(path, list);
}
public <T> void setExplicitList(final String path, final List<T> list, final Type type) {
try {
toSplitRoot(path, configurationNode).set(type, list);
} catch (SerializationException e) {
Essentials.getWrappedLogger().log(Level.SEVERE, e.getMessage(), e);
}
}
public <T> List<T> getList(final String path, Class<T> type) {
final CommentedConfigurationNode node = getInternal(path);
if (node == null) {
return new ArrayList<>();
}
try {
final List<T> list = node.getList(type);
if (list == null) {
return new ArrayList<>();
}
return list;
} catch (SerializationException e) {
Essentials.getWrappedLogger().log(Level.SEVERE, e.getMessage(), e);
return new ArrayList<>();
}
}
public boolean isList(String path) {
final CommentedConfigurationNode node = getInternal(path);
return node != null && node.isList();
}
public void setProperty(final String path, final String value) {
setInternal(path, value);
}
public String getString(final String path, final String def) {
final CommentedConfigurationNode node = getInternal(path);
if (node == null) {
return def;
}
return node.getString();
}
public void setProperty(final String path, final boolean value) {
setInternal(path, value);
}
public boolean getBoolean(final String path, final boolean def) {
final CommentedConfigurationNode node = getInternal(path);
if (node == null) {
return def;
}
return node.getBoolean();
}
public boolean isBoolean(final String path) {
final CommentedConfigurationNode node = getInternal(path);
return node != null && node.raw() instanceof Boolean;
}
public void setProperty(final String path, final long value) {
setInternal(path, value);
}
public long getLong(final String path, final long def) {
final CommentedConfigurationNode node = getInternal(path);
if (node == null) {
return def;
}
return node.getLong();
}
public void setProperty(final String path, final int value) {
setInternal(path, value);
}
public int getInt(final String path, final int def) {
final CommentedConfigurationNode node = getInternal(path);
if (node == null) {
return def;
}
return node.getInt();
}
public void setProperty(final String path, final double value) {
setInternal(path, value);
}
public double getDouble(final String path, final double def) {
final CommentedConfigurationNode node = getInternal(path);
if (node == null) {
return def;
}
return node.getDouble();
}
public void setProperty(final String path, final float value) {
setInternal(path, value);
}
public float getFloat(final String path, final float def) {
final CommentedConfigurationNode node = getInternal(path);
if (node == null) {
return def;
}
return node.getFloat();
}
public void setProperty(final String path, final BigDecimal value) {
setInternal(path, value);
}
public BigDecimal getBigDecimal(final String path, final BigDecimal def) {
final CommentedConfigurationNode node = getInternal(path);
if (node == null) {
return def;
}
try {
return node.get(BigDecimal.class);
} catch (SerializationException e) {
return null;
}
}
public void setRaw(final String path, final Object value) {
setInternal(path, value);
}
public Object get(final String path) {
final CommentedConfigurationNode node = getInternal(path);
return node == null ? null : node.raw();
}
public CommentedConfigurationNode getSection(final String path) {
final CommentedConfigurationNode node = toSplitRoot(path, configurationNode);
if (node.virtual()) {
return null;
}
return node;
}
public CommentedConfigurationNode newSection() {
return loader.createNode();
}
public Set<String> getKeys() {
return ConfigurateUtil.getKeys(configurationNode);
}
public Map<String, CommentedConfigurationNode> getMap() {
return ConfigurateUtil.getMap(configurationNode);
}
public Map<String, String> getStringMap(String path) {
final CommentedConfigurationNode node = getInternal(path);
if (node == null || !node.isMap()) {
return Collections.emptyMap();
}
final Map<String, String> map = new LinkedHashMap<>();
for (Map.Entry<Object, CommentedConfigurationNode> entry : node.childrenMap().entrySet()) {
map.put(String.valueOf(entry.getKey()), String.valueOf(entry.getValue().rawScalar()));
}
return map;
}
public void removeProperty(String path) {
final CommentedConfigurationNode node = getInternal(path);
if (node != null) {
try {
node.set(null);
} catch (SerializationException e) {
Essentials.getWrappedLogger().log(Level.SEVERE, e.getMessage(), e);
}
}
}
private void setInternal(final String path, final Object value) {
try {
toSplitRoot(path, configurationNode).set(value);
} catch (SerializationException e) {
Essentials.getWrappedLogger().log(Level.SEVERE, e.getMessage(), e);
}
}
private CommentedConfigurationNode getInternal(final String path) {
final CommentedConfigurationNode node = toSplitRoot(path, configurationNode);
if (node.virtual()) {
return null;
}
return node;
}
public boolean hasProperty(final String path) {
return !toSplitRoot(path, configurationNode).isNull();
}
public CommentedConfigurationNode toSplitRoot(String path, final CommentedConfigurationNode node) {
if (path == null) {
return node;
}
path = path.startsWith(".") ? path.substring(1) : path;
return node.node(path.contains(".") ? path.split("\\.") : new Object[]{path});
}
public synchronized void load() {
if (pendingWrites.get() != 0) {
Essentials.getWrappedLogger().log(Level.INFO, "Parsing config file {0} has been aborted due to {1} current pending write(s).", new Object[]{configFile, pendingWrites.get()});
return;
}
if (configFile.getParentFile() != null && !configFile.getParentFile().exists()) {
if (!configFile.getParentFile().mkdirs()) {
Essentials.getWrappedLogger().log(Level.SEVERE, tl("failedToCreateConfig", configFile.toString()));
return;
}
}
if (!configFile.exists()) {
if (legacyFileExists()) {
convertLegacyFile();
} else if (altFileExists()) {
convertAltFile();
} else if (templateName != null) {
try (final InputStream is = resourceClass.getResourceAsStream(templateName)) {
Essentials.getWrappedLogger().log(Level.INFO, tl("creatingConfigFromTemplate", configFile.toString()));
Files.copy(is, configFile.toPath());
} catch (IOException e) {
Essentials.getWrappedLogger().log(Level.SEVERE, tl("failedToWriteConfig", configFile.toString()), e);
}
}
}
try {
configurationNode = loader.load();
} catch (final ParsingException e) {
final File broken = new File(configFile.getAbsolutePath() + ".broken." + System.currentTimeMillis());
if (configFile.renameTo(broken)) {
Essentials.getWrappedLogger().log(Level.SEVERE, "The file " + configFile + " is broken, it has been renamed to " + broken, e.getCause());
return;
}
Essentials.getWrappedLogger().log(Level.SEVERE, "The file " + configFile + " is broken. A backup file has failed to be created", e.getCause());
} catch (final ConfigurateException e) {
Essentials.getWrappedLogger().log(Level.SEVERE, e.getMessage(), e);
} finally {
// Something is wrong! We need a node! I hope the backup worked!
if (configurationNode == null) {
configurationNode = loader.createNode();
}
}
}
public boolean legacyFileExists() {
return false;
}
public void convertLegacyFile() {
}
public boolean altFileExists() {
return false;
}
public void convertAltFile() {
}
/**
* Begins a transaction.
* <p>
* A transaction informs Essentials to pause the saving of data. This is should be used when
* bulk operations are being done and data shouldn't be saved until after the transaction has
* been completed.
*/
public void startTransaction() {
transaction.set(true);
}
public void stopTransaction() {
stopTransaction(false);
}
public void stopTransaction(final boolean blocking) {
transaction.set(false);
if (blocking) {
blockingSave();
} else {
save();
}
}
public boolean isTransaction() {
return transaction.get();
}
public void setSaveHook(Runnable saveHook) {
this.saveHook = saveHook;
}
public synchronized void save() {
if (!transaction.get()) {
delaySave();
}
}
public synchronized void blockingSave() {
try {
delaySave().get();
} catch (final InterruptedException | ExecutionException e) {
Essentials.getWrappedLogger().log(Level.SEVERE, e.getMessage(), e);
}
}
private Future<?> delaySave() {
if (saveHook != null) {
saveHook.run();
}
final CommentedConfigurationNode node = configurationNode.copy();
pendingWrites.incrementAndGet();
return EXECUTOR_SERVICE.submit(new ConfigurationSaveTask(loader, node, pendingWrites));
}
}