io.github.lucaseasedup.logit.config.PredefinedConfiguration.java Source code

Java tutorial

Introduction

Here is the source code for io.github.lucaseasedup.logit.config.PredefinedConfiguration.java

Source

/*
 * PredefinedConfiguration.java
 *
 * Copyright (C) 2012-2014 LucasEasedUp
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
 */
package io.github.lucaseasedup.logit.config;

import static io.github.lucaseasedup.logit.message.MessageHelper.t;
import com.google.common.collect.ImmutableMap;
import com.google.common.io.Files;
import io.github.lucaseasedup.logit.util.IniUtils;
import io.github.lucaseasedup.logit.util.IoUtils;
import it.sauronsoftware.base64.Base64;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.URLDecoder;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.logging.Level;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.zip.ZipEntry;
import java.util.zip.ZipFile;
import org.apache.commons.lang.StringUtils;
import org.bukkit.Color;
import org.bukkit.configuration.ConfigurationSection;
import org.bukkit.configuration.file.FileConfiguration;
import org.bukkit.configuration.file.YamlConfiguration;
import org.bukkit.inventory.ItemStack;
import org.bukkit.util.Vector;

public final class PredefinedConfiguration extends PropertyObserver implements PropertyHolder {
    public PredefinedConfiguration(String filename, String userDefPathname, String packageDefPathname,
            String header) {
        if (StringUtils.isBlank(filename) || userDefPathname == null || packageDefPathname == null) {
            throw new IllegalArgumentException();
        }

        this.file = getDataFile(filename);
        this.userDefPathname = userDefPathname;
        this.packageDefPathname = packageDefPathname;
        this.header = header;
    }

    @Override
    public void dispose() {
        configuration = null;

        if (properties != null) {
            for (Property property : properties.values()) {
                property.dispose();
            }

            properties.clear();
            properties = null;
        }
    }

    public void load() throws IOException, InvalidPropertyValueException {
        reopen();

        File backupFile = new File(file.getCanonicalPath() + ".bak");
        Files.copy(file, backupFile);

        String packageDefString = readPackageDefString();
        String userDefString;

        Map<String, Map<String, String>> packageDef = IniUtils.unserialize(packageDefString);
        Map<String, Map<String, String>> userDef;

        File userDefFile = getDataFile(userDefPathname);
        boolean userDefChanged = false;

        if (!userDefFile.exists()) {
            userDefFile.getParentFile().mkdirs();
            userDefString = packageDefString;
            userDef = IoUtils.deepCopy(packageDef);

            for (Map<String, String> userDefSection : userDef.values()) {
                registerProperty(userDefSection);
            }

            userDefChanged = true;
        } else {
            userDefString = readUserDefString();
            userDef = IniUtils.unserialize(userDefString);

            // Backup all user-specified values
            YamlConfiguration backup = YamlConfiguration.loadConfiguration(file);

            // Clear the config
            for (String path : configuration.getKeys(true)) {
                removePath(configuration, path);
            }

            /* Remove all unused sections from the user def */
            Iterator<Map.Entry<String, Map<String, String>>> it = userDef.entrySet().iterator();

            while (it.hasNext()) {
                Map.Entry<String, Map<String, String>> section = it.next();

                if (!packageDef.containsKey(section.getKey())) {
                    removePath(backup, section.getValue().get("path"));

                    it.remove();

                    userDefChanged = true;
                }
            }

            /* Iterate through the package def to update the user def and config file */
            for (Map.Entry<String, Map<String, String>> section : packageDef.entrySet()) {
                String guid = section.getKey();
                Map<String, String> packageDefSection = section.getValue();
                Map<String, String> userDefSection = userDef.get(guid);

                String newPath = packageDefSection.get("path");
                String oldPath;

                if (userDefSection == null) {
                    oldPath = newPath;

                    userDefSection = new LinkedHashMap<>(packageDefSection);
                    userDef.put(guid, userDefSection);
                    userDefChanged = true;
                } else {
                    oldPath = userDefSection.get("path");

                    for (Map.Entry<String, String> property : packageDefSection.entrySet()) {
                        String key = property.getKey();

                        if (!property.getValue().equals(userDefSection.get(key))) {
                            userDefSection.put(key, property.getValue());
                            userDefChanged = true;
                        }
                    }
                }

                registerProperty(userDefSection);

                /* Restore user-specified value */
                if (backup.contains(oldPath)) {
                    Object backupValue = backup.get(oldPath);

                    configuration.set(newPath, backupValue);
                    properties.get(newPath).setSilently(backupValue);

                    removePath(backup, oldPath);
                }
            }

            userDefString = IniUtils.serialize(userDef);
        }

        if (userDefChanged) {
            try (OutputStream userDefOutputStream = new FileOutputStream(userDefFile)) {
                userDefOutputStream.write(encodeUserDef(userDefString).getBytes());
            }
        }

        save();
        backupFile.delete();

        loaded = true;
    }

    private String readPackageDefString() throws IOException {
        String jarUrlPath = getClass().getProtectionDomain().getCodeSource().getLocation().getPath();
        String jarPath = URLDecoder.decode(jarUrlPath, "UTF-8");

        try (ZipFile jarZipFile = new ZipFile(jarPath)) {
            ZipEntry packageDefEntry = jarZipFile.getEntry(packageDefPathname);

            try (InputStream packageDefInputStream = jarZipFile.getInputStream(packageDefEntry)) {
                return IoUtils.toString(packageDefInputStream);
            }
        }
    }

    private String readUserDefString() throws IOException {
        try (InputStream userDefInputStream = new FileInputStream(getDataFile(userDefPathname))) {
            return decodeConfigDef(IoUtils.toString(userDefInputStream));
        }
    }

    private void reopen() throws IOException {
        if (!file.exists()) {
            file.getParentFile().mkdirs();
            file.createNewFile();
        }

        configuration = YamlConfiguration.loadConfiguration(file);

        if (header != null) {
            configuration.options().header(header);
        }
    }

    /**
     * Saves the config to a predefined file.
     * 
     * @throws IOException if an I/O error occurred.
     */
    public void save() throws IOException {
        configuration.save(file);
    }

    @Override
    public Map<String, Property> getProperties() {
        return ImmutableMap.copyOf(properties);
    }

    /**
     * Returns a property object at the given path.
     * 
     * @param path the path.
     * 
     * @return the property object.
     */
    @Override
    public Property getProperty(String path) {
        return properties.get(path);
    }

    /**
     * Checks if the config contains a property at the given path.
     * 
     * @param path the path.
     * 
     * @return {@code true} if such property exists; {@code false} otherwise.
     */
    @Override
    public boolean contains(String path) {
        return properties.containsKey(path);
    }

    @Override
    public Set<String> getKeys(String path) {
        return configuration.getConfigurationSection(path).getKeys(false);
    }

    @Override
    public Map<String, Object> getValues(String path) {
        Map<String, Object> values = new LinkedHashMap<>();

        for (String key : getKeys(path)) {
            values.put(key, getString(path + "." + key));
        }

        return values;
    }

    @Override
    public Object get(String path) {
        return properties.get(path);
    }

    @Override
    public boolean getBoolean(String path) {
        return (Boolean) properties.get(path).getValue();
    }

    @Override
    @SuppressWarnings("unchecked")
    public List<Boolean> getBooleanList(String path) {
        return (List<Boolean>) properties.get(path).getValue();
    }

    @Override
    @SuppressWarnings("unchecked")
    public List<Byte> getByteList(String path) {
        return (List<Byte>) properties.get(path).getValue();
    }

    @Override
    @SuppressWarnings("unchecked")
    public List<Character> getCharacterList(String path) {
        return (List<Character>) properties.get(path).getValue();
    }

    @Override
    public Color getColor(String path) {
        return (Color) properties.get(path).getValue();
    }

    @Override
    public double getDouble(String path) {
        return (Double) properties.get(path).getValue();
    }

    @Override
    @SuppressWarnings("unchecked")
    public List<Double> getDoubleList(String path) {
        return (List<Double>) properties.get(path).getValue();
    }

    @Override
    @SuppressWarnings("unchecked")
    public List<Float> getFloatList(String path) {
        return (List<Float>) properties.get(path).getValue();
    }

    @Override
    public int getInt(String path) {
        return (Integer) properties.get(path).getValue();
    }

    @Override
    @SuppressWarnings("unchecked")
    public List<Integer> getIntegerList(String path) {
        return (List<Integer>) properties.get(path).getValue();
    }

    @Override
    public ItemStack getItemStack(String path) {
        return (ItemStack) properties.get(path).getValue();
    }

    @Override
    public List<?> getList(String path) {
        return (List<?>) properties.get(path).getValue();
    }

    @Override
    public long getLong(String path) {
        return (Long) properties.get(path).getValue();
    }

    @Override
    @SuppressWarnings("unchecked")
    public List<Long> getLongList(String path) {
        return (List<Long>) properties.get(path).getValue();
    }

    @Override
    @SuppressWarnings("unchecked")
    public List<Map<?, ?>> getMapList(String path) {
        return (List<Map<?, ?>>) properties.get(path).getValue();
    }

    @Override
    @SuppressWarnings("unchecked")
    public List<Short> getShortList(String path) {
        return (List<Short>) properties.get(path).getValue();
    }

    @Override
    public String getString(String path) {
        return (String) properties.get(path).getValue();
    }

    @Override
    @SuppressWarnings("unchecked")
    public List<String> getStringList(String path) {
        return (List<String>) properties.get(path).getValue();
    }

    @Override
    public Vector getVector(String path) {
        return (Vector) properties.get(path).getValue();
    }

    @Override
    public LocationSerializable getLocation(String path) {
        return (LocationSerializable) properties.get(path).getValue();
    }

    @Override
    public long getTime(String path, TimeUnit convertTo) {
        if (path == null || convertTo == null)
            throw new IllegalArgumentException();

        return TimeString.decode(getString(path), convertTo);
    }

    /**
     * Sets a new value for a property at the given path.
     * 
     * @param path  the property path.
     * @param value the new value.
     * 
     * @throws InvalidPropertyValueException if the value provided
     *                                       is not valid for this property.
     */
    @Override
    public void set(String path, Object value) throws InvalidPropertyValueException {
        properties.get(path).set(value);
    }

    /**
     * Internal method. Do not call directly.
     */
    @Override
    public void update(Property p) {
        configuration.set(p.getPath(), p.getValue());

        try {
            save();

            log(Level.INFO, t("config.set.success.log").replace("{0}", p.getPath()).replace("{1}", p.toString()));
        } catch (IOException ex) {
            log(Level.WARNING, t("config.set.fail.log"), ex);
        }
    }

    /**
     * Checks if the config has been successfully loaded.
     * 
     * @return {@code true} if the config has been loaded; {@code false} otherwise.
     */
    public boolean isLoaded() {
        return loaded;
    }

    private void removePath(ConfigurationSection section, String path) {
        section.set(path, null);

        if (!path.contains(".")) {
            return;
        }

        String parentPath = path.substring(0, path.lastIndexOf('.'));

        if (properties.containsKey(parentPath)) {
            return;
        }

        ConfigurationSection parentSection = section.getConfigurationSection(parentPath);

        if (parentSection == null || !parentSection.getKeys(false).isEmpty()) {
            return;
        }

        removePath(section, parentPath);
    }

    private void registerProperty(String path, PropertyType type, boolean requiresRestart, Object defaultValue,
            PropertyValidator validator, PropertyObserver obs) throws InvalidPropertyValueException {
        Object existingValue = configuration.get(path, defaultValue);
        Property property = new Property(path, type, requiresRestart, existingValue, validator);

        if (obs != null) {
            property.addObserver(obs);
        }

        property.addObserver(this);

        Object value;

        if (!configuration.isConfigurationSection(path)) {
            value = property.getValue();
        } else {
            value = defaultValue;
        }

        if (validator != null && !validator.validate(path, type, value)) {
            throw new InvalidPropertyValueException(path);
        }

        configuration.set(property.getPath(), value);
        properties.put(property.getPath(), property);
    }

    private void registerProperty(Map<String, String> defSection) throws InvalidPropertyValueException {
        String path = defSection.get("path");
        PropertyType type;
        boolean requiresRestart = Boolean.valueOf(defSection.get("requires_restart"));
        Object defaultValue;
        PropertyValidator validator = null;
        PropertyObserver observer = null;

        String typeString = defSection.get("type");

        try {
            type = PropertyType.valueOf(typeString);
        } catch (IllegalArgumentException ex) {
            log(Level.WARNING, "Unknown property type: " + typeString);

            return;
        }

        String defaultValueString = defSection.get("default_value");

        switch (type) {
        case CONFIGURATION_SECTION: {
            defaultValue = configuration.getConfigurationSection(path);

            if (defaultValue == null) {
                defaultValue = configuration.createSection(path);
            }

            break;
        }
        case OBJECT:
            defaultValue = null;
            break;

        case BOOLEAN:
            defaultValue = Boolean.valueOf(defaultValueString);
            break;

        case COLOR: {
            switch (defaultValueString.toLowerCase()) {
            case "aqua":
                defaultValue = Color.AQUA;
                break;
            case "black":
                defaultValue = Color.BLACK;
                break;
            case "blue":
                defaultValue = Color.BLUE;
                break;
            case "fuchsia":
                defaultValue = Color.FUCHSIA;
                break;
            case "gray":
                defaultValue = Color.GRAY;
                break;
            case "green":
                defaultValue = Color.GREEN;
                break;
            case "lime":
                defaultValue = Color.LIME;
                break;
            case "maroon":
                defaultValue = Color.MAROON;
                break;
            case "navy":
                defaultValue = Color.NAVY;
                break;
            case "olive":
                defaultValue = Color.OLIVE;
                break;
            case "orange":
                defaultValue = Color.ORANGE;
                break;
            case "purple":
                defaultValue = Color.PURPLE;
                break;
            case "red":
                defaultValue = Color.RED;
                break;
            case "silver":
                defaultValue = Color.SILVER;
                break;
            case "teal":
                defaultValue = Color.TEAL;
                break;
            case "white":
                defaultValue = Color.WHITE;
                break;
            case "yellow":
                defaultValue = Color.YELLOW;
                break;
            default: {
                String[] rgb = defaultValueString.split(" ");

                if (rgb.length == 3) {
                    defaultValue = Color.fromRGB(Integer.parseInt(rgb[0]), Integer.parseInt(rgb[1]),
                            Integer.parseInt(rgb[2]));
                } else {
                    defaultValue = Color.BLACK;
                }

                break;
            }
            }

            break;
        }
        case DOUBLE:
            defaultValue = Double.valueOf(defaultValueString);
            break;

        case INT:
            defaultValue = Integer.valueOf(defaultValueString);
            break;

        case ITEM_STACK:
            defaultValue = null;
            break;

        case LONG:
            defaultValue = Long.valueOf(defaultValueString);
            break;

        case STRING:
            defaultValue = defaultValueString;
            break;

        case VECTOR: {
            String[] axes = defaultValueString.split(" ");

            if (axes.length == 3) {
                defaultValue = new Vector(Double.valueOf(axes[0]), Double.valueOf(axes[1]),
                        Double.valueOf(axes[2]));
            } else {
                defaultValue = new Vector(0, 0, 0);
            }

            break;
        }
        case LIST:
        case BOOLEAN_LIST:
        case BYTE_LIST:
        case CHARACTER_LIST:
        case DOUBLE_LIST:
        case FLOAT_LIST:
        case INTEGER_LIST:
        case LONG_LIST:
        case MAP_LIST:
        case SHORT_LIST:
        case STRING_LIST:
            defaultValue = new ArrayList<>(0);
            break;

        case LOCATION:
            defaultValue = new LocationSerializable("world", 0, 0, 0, 0, 0);
            break;

        default:
            throw new RuntimeException("Unknown property type: " + type.toString());
        }

        String validatorClassName = defSection.get("validator");

        if (!StringUtils.isBlank(validatorClassName)) {
            try {
                @SuppressWarnings("unchecked")
                Class<PropertyValidator> validatorClass = (Class<PropertyValidator>) Class
                        .forName(validatorClassName);

                validator = validatorClass.getConstructor().newInstance();
            } catch (ReflectiveOperationException ex) {
                log(Level.WARNING, "Invalid property validator: " + validatorClassName + ".", ex);

                return;
            }
        }

        String observerClassName = defSection.get("observer");

        if (!StringUtils.isBlank(observerClassName)) {
            try {
                @SuppressWarnings("unchecked")
                Class<PropertyObserver> observerClass = (Class<PropertyObserver>) Class.forName(observerClassName);

                observer = observerClass.getConstructor().newInstance();
            } catch (ReflectiveOperationException ex) {
                log(Level.WARNING, "Invalid property observer: " + observerClassName + ".", ex);

                return;
            }
        }

        registerProperty(path, type, requiresRestart, defaultValue, validator, observer);
    }

    private String encodeUserDef(String input) {
        return Base64.encode(input);
    }

    private String decodeConfigDef(String input) {
        return Base64.decode(input);
    }

    /**
     * Converts a hyphenated path (example-path.secret-setting)
     * to a camelCase path (examplePath.secretSetting).
     * 
     * @param hyphenatedPath the hyphenated path.
     * 
     * @return the camelCase equivalent of the provided hyphenated path.
     */
    public static String getCamelCasePath(String hyphenatedPath) {
        Matcher matcher = HYPHENATED_PATH_PATTERN.matcher(hyphenatedPath);
        StringBuilder sb = new StringBuilder();
        int last = 0;

        while (matcher.find()) {
            sb.append(hyphenatedPath.substring(last, matcher.start()));
            sb.append(matcher.group(1).toUpperCase());

            last = matcher.end();
        }

        sb.append(hyphenatedPath.substring(last));

        return sb.toString();
    }

    private static final Pattern HYPHENATED_PATH_PATTERN = Pattern.compile("\\-([a-z])");

    private final File file;
    private FileConfiguration configuration;
    private final String userDefPathname;
    private final String packageDefPathname;
    private final String header;
    private boolean loaded = false;
    private Map<String, Property> properties = new LinkedHashMap<>();
}