com.flowpowered.cerealization.config.ini.IniConfiguration.java Source code

Java tutorial

Introduction

Here is the source code for com.flowpowered.cerealization.config.ini.IniConfiguration.java

Source

/*
 * This file is part of Flow Cerealization, licensed under the MIT License (MIT).
 *
 * Copyright (c) 2013 Spout LLC <https://spout.org/>
 *
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to deal
 * in the Software without restriction, including without limitation the rights
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in
 * all copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
 * THE SOFTWARE.
 */
package com.flowpowered.cerealization.config.ini;

import java.io.BufferedReader;
import java.io.BufferedWriter;
import java.io.File;
import java.io.IOException;
import java.io.Reader;
import java.io.Writer;
import java.lang.reflect.Array;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import org.apache.commons.lang3.ArrayUtils;
import org.apache.commons.lang3.StringUtils;

import com.flowpowered.cerealization.config.AbstractConfiguration;
import com.flowpowered.cerealization.config.ConfigurationException;
import com.flowpowered.cerealization.config.ConfigurationNode;
import com.flowpowered.cerealization.config.FileConfiguration;
import com.flowpowered.cerealization.config.commented.CommentedConfiguration;
import com.flowpowered.cerealization.config.commented.CommentedConfigurationNode;
import com.flowpowered.cerealization.data.IOFactory;

import static com.flowpowered.cerealization.config.commented.CommentedConfigurationNode.LINE_SEPARATOR;

/**
 * This class handles reading and writing configuration nodes in the INI format. Because the INI format is fairly loose, this class can read INI files written with a few formats. However, when writing
 * INI files the configuration's settings will override what was previously in the file
 *
 * The INI format also has a few limitations over other formats. <ul> <li>Limited hierarchy: The format can only have one level of children (sections)</li> <li>Extremely basic datatype support: The
 * configuration can split up lists, but otherwise all values are stored as strings. Maps are not supported.</li> <li>Saving files exactly as they were loaded is in some cases impossible. </li> </ul>
 *
 * An example INI file is as follows:
 * <pre>
 *     # All nodes outside of a section are given above the first section specifier
 *     # The : character is a valid key-value splitter, but this implementation currently only writes with the '=' splitter
 *     sectionless-node: see here!
 *
 *     # This is a section
 *     # These sections are the only way of splitting up configuration settings
 *     [section]
 *     key=value
 *     anotherkey = list, of, values
 *
 *
 *     [anothersection]
 *     # Because this value is surrounded by quotes, this is not a list
 *     # Both sections and keys can have comments. These comments are read and written from the configuration file
 *     keys = "not, a, list"
 *
 * </pre>
 */
public class IniConfiguration extends AbstractConfiguration implements CommentedConfiguration, FileConfiguration {
    public static final char COMMENT_CHAR_SEMICOLON = ';';
    public static final char COMMENT_CHAR_HASH = '#';
    public static final Pattern COMMENT_REGEX = Pattern
            .compile("[" + COMMENT_CHAR_SEMICOLON + COMMENT_CHAR_HASH + "] ?(.*)");
    public static final Pattern SECTION_REGEX = Pattern.compile("\\[(.*)\\]");
    private char preferredCommentChar = COMMENT_CHAR_HASH;
    private final IOFactory factory;

    public IniConfiguration(File file) {
        this(new IOFactory.File(file));
    }

    public IniConfiguration(IOFactory factory) {
        this.factory = factory;
    }

    @Override
    protected Map<String, ConfigurationNode> loadToNodes() throws ConfigurationException {
        Reader stream = null;
        Map<String, ConfigurationNode> nodes = new LinkedHashMap<String, ConfigurationNode>();
        try {
            stream = getReader();
            BufferedReader reader = new BufferedReader(stream);
            String line;
            List<String> comments = new ArrayList<String>();
            List<String> curSection = new ArrayList<String>();
            CommentedConfigurationNode node = null;
            Matcher match;
            while ((line = reader.readLine()) != null) {
                line = line.trim();
                if (line.length() == 0) {
                    continue;
                }

                match = COMMENT_REGEX.matcher(line);
                if (match.matches()) {
                    comments.add(match.group(1));
                    continue;
                }
                match = SECTION_REGEX.matcher(line);
                if (match.matches()) {
                    if (node != null) {
                        for (ConfigurationNode subNode : readNodeSection(node.getPathElements(),
                                curSection.toArray(new String[curSection.size()]))) {
                            node.addChild(subNode);
                        }
                    }
                    node = createConfigurationNode(new String[] { match.group(1) }, null);
                    if (comments.size() > 0) {
                        node.setComment(comments.toArray(new String[comments.size()]));
                        comments.clear();
                    }
                    nodes.put(match.group(1), node);
                } else {
                    if (comments.size() > 0) {
                        for (String comment : comments) {
                            curSection.add(getPreferredCommentChar() + " " + comment);
                        }
                        comments.clear();
                    }
                    curSection.add(line);
                }
            }

            if (node != null && curSection.size() > 0) {
                for (ConfigurationNode subNode : readNodeSection(node.getPathElements(),
                        curSection.toArray(new String[curSection.size()]))) {
                    node.addChild(subNode);
                }
            }
        } catch (IOException e) {
            throw new ConfigurationException(e);
        } finally {
            if (stream != null) {
                try {
                    stream.close();
                } catch (IOException ignore) {
                }
            }
        }
        return nodes;
    }

    @Override
    protected void saveFromNodes(Map<String, ConfigurationNode> nodes) throws ConfigurationException {
        Writer rawWriter = null;
        BufferedWriter writer = null;
        try {
            rawWriter = getWriter();
            writer = new BufferedWriter(rawWriter);

            List<ConfigurationNode> childlessNodes = new ArrayList<ConfigurationNode>(),
                    sectionNodes = new ArrayList<ConfigurationNode>();
            for (ConfigurationNode node : nodes.values()) {
                if (node.hasChildren()) {
                    sectionNodes.add(node);
                } else {
                    childlessNodes.add(node);
                }
            }

            if (childlessNodes.size() > 0) {
                writeNodeSection(writer, childlessNodes);
            }

            for (Iterator<ConfigurationNode> i = sectionNodes.iterator(); i.hasNext();) {
                ConfigurationNode node = i.next();
                String[] comment = getComment(node);
                if (comment != null) {
                    for (String line : comment) {
                        writer.append(getPreferredCommentChar()).append(" ").append(line).append(LINE_SEPARATOR);
                    }
                }
                writer.append('[').append(node.getPathElements()[0]).append(']').append(LINE_SEPARATOR);
                writeNodeSection(writer, node.getChildren().values());
                if (i.hasNext()) {
                    writer.append(LINE_SEPARATOR);
                }
            }
        } catch (IOException e) {
            throw new ConfigurationException(e);
        } finally {
            if (writer != null) {
                try {
                    writer.flush();
                } catch (IOException ignore) {
                }
            }
            if (rawWriter != null) {
                try {
                    rawWriter.flush();
                    rawWriter.close();
                } catch (IOException ignore) {
                }
            }
        }
    }

    /**
     * This method reads one section of INI configuration data.
     *
     * @param parentPath The path of the section containing this data
     * @param lines The lines of data to read
     * @return The configuration nodes read from the section
     * @throws ConfigurationException when an invalid node is specified
     */
    protected List<ConfigurationNode> readNodeSection(String[] parentPath, String[] lines)
            throws ConfigurationException {
        List<ConfigurationNode> nodes = new ArrayList<ConfigurationNode>();
        List<String> comment = new ArrayList<String>();
        Matcher match;
        for (String line : lines) {
            match = COMMENT_REGEX.matcher(line);
            if (match.matches()) {
                comment.add(match.group(1));
                continue;
            }
            String[] split = line.split("[=:]", 2);
            if (split.length < 2) {
                throw new ConfigurationException("Key with no value: " + line);
            }
            CommentedConfigurationNode node = createConfigurationNode(ArrayUtils.add(parentPath, split[0].trim()),
                    null);
            node.setValue(fromStringValue(split[1].trim()));
            if (comment.size() > 0) {
                node.setComment(comment.toArray(new String[comment.size()]));
            }
            nodes.add(node);
        }
        return nodes;
    }

    /**
     * Writes a single section of nodes to the specified Writer The nodes passed to this method must not have children
     *
     * @param writer The Writer to write data to
     * @param nodes The nodes to write
     * @throws ConfigurationException when a node cannot be correctly written
     */
    protected void writeNodeSection(Writer writer, Collection<ConfigurationNode> nodes)
            throws ConfigurationException {
        try {
            for (ConfigurationNode node : nodes) {
                if (node.hasChildren()) {
                    throw new ConfigurationException("Nodes passed to getChildlessNodes must not have children!");
                }
                String[] comment = getComment(node);
                if (comment != null) {
                    for (String line : comment) {
                        writer.append(getPreferredCommentChar()).append(" ").append(line).append(LINE_SEPARATOR);
                    }
                }
                writer.append(node.getPathElements()[node.getPathElements().length - 1]).append("=")
                        .append(toStringValue(node.getValue())).append(LINE_SEPARATOR);
            }
        } catch (IOException e) {
            throw new ConfigurationException(e);
        }
    }

    /**
     * Returns the comment for a given configuration node, with a safe check to make sure the node is a CommentedConfigurationNode
     *
     * @param node The node to get a comment from
     * @return The node's comment, or null if no comment is present
     */
    protected static String[] getComment(ConfigurationNode node) {
        String[] comment = null;
        if (node instanceof CommentedConfigurationNode) {
            comment = ((CommentedConfigurationNode) node).getComment();
        }
        return comment;
    }

    /**
     * Converts a raw String into the correct Object representation for the Configuration node
     *
     * @param value The string value
     * @return The value converted into the correct Object representation
     */
    public Object fromStringValue(String value) {
        if (value.matches("^([\"']).*\\1$")) { // Quote value
            return value.substring(1, value.length() - 1);
        }

        String[] objects = value.split(", ?");
        if (objects.length == 1) {
            return objects[0];
        }
        return new ArrayList<String>(Arrays.asList(objects));
    }

    /**
     * Returns the String representation of a configuration value for writing to the file
     *
     * @return string representation
     */
    public String toStringValue(Object value) {
        if (value == null) {
            return "null";
        }
        if (value.getClass().isArray()) {
            List<Object> toList = new ArrayList<Object>();
            final int length = Array.getLength(value);
            for (int i = 0; i < length; ++i) {
                toList.add(Array.get(value, i));
            }
            value = toList;
        }
        if (value instanceof Collection) {
            StringBuilder builder = new StringBuilder();
            for (Object obj : (Collection<?>) value) {
                if (builder.length() > 0) {
                    builder.append(", ");
                }
                builder.append(obj.toString());
            }
            return builder.toString();
        } else {
            String strValue = value.toString();
            if (strValue.contains(",")) {
                strValue = '"' + strValue + '"';
            }
            return strValue;
        }
    }

    @Override
    public CommentedConfigurationNode getNode(String path) {
        return (CommentedConfigurationNode) super.getNode(path);
    }

    @Override
    public CommentedConfigurationNode getNode(String... path) {
        return (CommentedConfigurationNode) super.getNode(path);
    }

    @Override
    public String[] splitNodePath(String path) {
        return getPathSeparatorPattern().split(path, 2);
    }

    @Override
    public String[] ensureCorrectPath(String[] rawPath) {
        if (rawPath.length <= 2) {
            return rawPath;
        }

        return new String[] { rawPath[0],
                StringUtils.join(ArrayUtils.subarray(rawPath, 1, rawPath.length), getPathSeparator()) };
    }

    @Override
    public CommentedConfigurationNode createConfigurationNode(String[] path, Object value) {
        return new CommentedConfigurationNode(getConfiguration(), path, value);
    }

    public IOFactory getIOFactory() {
        return factory;
    }

    protected Reader getReader() throws IOException {
        return factory.createReader();
    }

    protected Writer getWriter() throws IOException {
        return factory.createWriter();
    }

    @Override
    public File getFile() {
        return factory instanceof IOFactory.File ? ((IOFactory.File) factory).getFile() : null;
    }

    public char getPreferredCommentChar() {
        return preferredCommentChar;
    }

    public void setPreferredCommentChar(char commentChar) {
        if (commentChar != COMMENT_CHAR_HASH && commentChar != COMMENT_CHAR_SEMICOLON) {
            throw new IllegalArgumentException("Invalid comment char: " + commentChar + "!");
        }
        this.preferredCommentChar = commentChar;
    }
}