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 getLocationSectionMap(final String path) { final CommentedConfigurationNode node = getSection(path); final Map result = new HashMap<>(); for (final Map.Entry 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 void setExplicitList(final String path, final List list, final Type type) { try { toSplitRoot(path, configurationNode).set(type, list); } catch (SerializationException e) { Essentials.getWrappedLogger().log(Level.SEVERE, e.getMessage(), e); } } public List getList(final String path, Class type) { final CommentedConfigurationNode node = getInternal(path); if (node == null) { return new ArrayList<>(); } try { final List 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 getKeys() { return ConfigurateUtil.getKeys(configurationNode); } public Map getMap() { return ConfigurateUtil.getMap(configurationNode); } public Map getStringMap(String path) { final CommentedConfigurationNode node = getInternal(path); if (node == null || !node.isMap()) { return Collections.emptyMap(); } final Map map = new LinkedHashMap<>(); for (Map.Entry 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. *

* 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)); } }