/*
* Copyright 2011 ZerothAngel <zerothangel@tyrannyofheaven.org>
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.tyrannyofheaven.bukkit.util;
import static org.tyrannyofheaven.bukkit.util.ToHLoggingUtils.log;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.io.Reader;
import java.util.HashMap;
import java.util.Map;
import java.util.logging.Level;
import org.bukkit.configuration.Configuration;
import org.bukkit.configuration.InvalidConfigurationException;
import org.bukkit.configuration.file.FileConfiguration;
import org.bukkit.configuration.file.YamlConfiguration;
import org.bukkit.plugin.Plugin;
import org.tyrannyofheaven.bukkit.util.configuration.AnnotatedYamlConfiguration;
import com.google.common.base.Charsets;
/**
* File utilities.
*
* @author zerothangel
*/
public class ToHFileUtils {
// Size of buffer for copyFile()
private static final int COPY_BUFFER_SIZE = 4096;
// Configuration version property key
private static final String CONFIG_VERSION_KEY = "config-version";
private ToHFileUtils() {
throw new AssertionError("Don't instantiate me!");
}
/**
* Copy an InputStream to a file.
*
* @param input the InputStream
* @param outFile the output File
* @throws IOException
*/
public static void copyFile(InputStream input, File outFile) throws IOException {
OutputStream os = new FileOutputStream(outFile);
try {
byte[] buffer = new byte[COPY_BUFFER_SIZE];
int readLen;
while ((readLen = input.read(buffer)) != -1) {
os.write(buffer, 0, readLen);
}
}
finally {
os.close();
}
}
/**
* Copy a resource (using a class's classloader) to a file.
*
* @param clazz the class
* @param resourceName resource name relative to the class
* @param outFile the output File
* @throws IOException
*/
public static void copyResourceToFile(Class<?> clazz, String resourceName, File outFile) throws IOException {
InputStream is = clazz.getResourceAsStream(resourceName);
try {
copyFile(is, outFile);
}
finally {
is.close();
}
}
/**
* Copies a resource relative to the Plugin class to a file.
*
* @param plugin the plugin
* @param resourceName resource name relative to plugin's class
* @param outFile the output file
* @return true if successful, false otherwise
*/
public static boolean copyResourceToFile(Plugin plugin, String resourceName, File outFile) {
try {
copyResourceToFile(plugin.getClass(), resourceName, outFile);
return true;
}
catch (IOException e) {
log(plugin, Level.SEVERE, "Error copying %s to %s", resourceName, outFile, e);
return false;
}
}
/**
* Return the specified Configuration. The Configuration may pull its defaults
* from the given resource file. (Note: the resource is loaded relative to the
* plugin class.) The Configuration may also use comments read from the given
* comments file.
*
* @param plugin the plugin
* @param configDir the parent directory of the config file
* @param configName the name of the config file
* @param mustExist set to true if the file must exist (will throw FileNotFoundException if config not present)
* @param defaultsName the path of the defaults resource, relative to the plugin class. May be <code>null</code>.
* @param commentsName the path of the comments resource, relative to the plugin class. May be <code>null</code>.
* @return the Configuration object, with defaults and comments appropriately set
* @throws FileNotFoundException if mustExist is true and the config file is not found
*/
public static FileConfiguration getConfig(Plugin plugin, File configDir, String configName, boolean mustExist, String defaultsName, String commentsName) throws FileNotFoundException {
File configFile = new File(configDir, configName);
AnnotatedYamlConfiguration config = new AnnotatedYamlConfiguration();
try {
config.load(configFile);
}
catch (FileNotFoundException e) {
if (mustExist)
throw e;
// Otherwise, ignore...
}
catch (IOException | InvalidConfigurationException e) {
ToHLoggingUtils.error(plugin, "Error reading configuration %s", configFile, e);
}
// Set defaults if present
if (defaultsName != null) {
InputStream defaultsInput = plugin.getClass().getResourceAsStream(defaultsName);
if (defaultsInput != null) {
try (Reader reader = new InputStreamReader(defaultsInput, Charsets.UTF_8)) {
Configuration defaults = YamlConfiguration.loadConfiguration(reader);
config.setDefaults(defaults);
}
catch (IOException e) {
// Ignored (thrown by close)
}
}
}
// Set root-level comments, if appropriate file is present
if (commentsName != null) {
InputStream commentsInput = plugin.getClass().getResourceAsStream(commentsName);
if (commentsInput != null) {
try (Reader reader = new InputStreamReader(commentsInput, Charsets.UTF_8)) {
Configuration comments = YamlConfiguration.loadConfiguration(reader);
Map<String, String> commentsMap = new HashMap<>();
for (Map.Entry<String, Object> entry : comments.getValues(false).entrySet()) {
commentsMap.put(entry.getKey(), entry.getValue().toString());
}
config.setComments(commentsMap);
}
catch (IOException e) {
// Ignored (thrown by close)
}
}
}
return config;
}
/**
* Return the specified Configuration. The Configuration may pull its defaults
* from the given resource file. (Note: the resource is loaded relative to the
* plugin class.) The Configuration may also use comments read from the given
* comments file.
*
* @param plugin the plugin
* @param configDir the parent directory of the config file
* @param configName the name of the config file
* @param defaultsName the path of the defaults resource, relative to the plugin class. May be <code>null</code>.
* @param commentsName the path of the comments resource, relative to the plugin class. May be <code>null</code>.
* @return the Configuration object, with defaults and comments appropriately set
*/
public static FileConfiguration getConfig(Plugin plugin, File configDir, String configName, String defaultsName, String commentsName) {
try {
return getConfig(plugin, configDir, configName, false, defaultsName, commentsName);
}
catch (FileNotFoundException e) {
// Should never get here because we set mustExist to false
throw new AssertionError();
}
}
/**
* Fetch the plugin's standard Configuration (from <code>plugins/plugin-name/config.yml</code>)
* and set it up with the appropriate defaults and comments resources, if present.
* The defaults and comments resources are expected to be at config.yml and
* config-comments.yml respectively, in the same package as the plugin's class.
*
* @param plugin the plugin
* @return the Configuration object, appropriately configured with defaults and comments
*/
public static FileConfiguration getConfig(Plugin plugin) {
return getConfig(plugin, plugin.getDataFolder(), "config.yml", "config.yml", "config-comments.yml");
}
/**
* Attempt to save a FileConfiguration.
*
* @param plugin the plugin
* @param config the FileConfiguration to save
* @param configDir the parent directory of the file
* @param configName the config filename
*/
public static void saveConfig(Plugin plugin, FileConfiguration config, File configDir, String configName) {
File newConfigFile = new File(configDir, configName + ".new");
// First try saving
try {
config.save(newConfigFile);
}
catch (IOException e) {
ToHLoggingUtils.error(plugin, "Error saving configuration %s", newConfigFile, e);
return;
}
File backupConfigFile = new File(configDir, configName + "~");
// Delete old backup (might be necessary on some platforms)
if (backupConfigFile.exists() && !backupConfigFile.delete()) {
ToHLoggingUtils.error(plugin, "Error deleting configuration %s", backupConfigFile);
// Continue despite failure
}
File configFile = new File(configDir, configName);
// If only we had access to hardlinks, this could all be atomic.
// Back up old config
if (configFile.exists() && !configFile.renameTo(backupConfigFile)) {
ToHLoggingUtils.error(plugin, "Error renaming %s to %s", configFile, backupConfigFile);
return; // no backup, abort
}
// Rename new file to config
if (!newConfigFile.renameTo(configFile)) {
ToHLoggingUtils.error(plugin, "Error renaming %s to %s", newConfigFile, configFile);
// Nothing else to do
}
}
/**
* Save a FileConfiguration as the plugin's standard config.yml.
*
* @param plugin the plugin
* @param config the FileConfiguration
*/
public static void saveConfig(Plugin plugin, FileConfiguration config) {
saveConfig(plugin, config, plugin.getDataFolder(), "config.yml");
}
/**
* Upgrade the standard configuration file, if necessary.
*
* @param plugin the plugin
* @param config the FileConfiguration
* @param currentVersion the expected version, should be > 0
*/
public static void upgradeConfig(Plugin plugin, FileConfiguration config) {
Configuration defaults = config.getDefaults();
if (defaults == null)
throw new IllegalStateException("config does not have defaults");
int currentVersion = defaults.getInt(CONFIG_VERSION_KEY);
int configVersion = config.getInt(CONFIG_VERSION_KEY);
if (!config.isSet(CONFIG_VERSION_KEY) || configVersion < currentVersion) {
ToHLoggingUtils.log(plugin, "Upgrading config.yml");
// Update version
config.set(CONFIG_VERSION_KEY, currentVersion);
// Save old copyDefaults value & set to true
boolean copyDefaults = config.options().copyDefaults();
config.options().copyDefaults(true);
// Save config
saveConfig(plugin, config);
// Restore old copyDefaults
config.options().copyDefaults(copyDefaults);
}
}
}