Java tutorial
/* * Copyright (C) 2012 CyborgDev <cyborg@alta189.com> * * This file is part of Cyborg * * Cyborg 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. * * Cyborg 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. If not, see <http://www.gnu.org/licenses/>. */ package com.alta189.cyborg.api.util.config.ini; import com.alta189.cyborg.api.exception.ConfigurationException; import com.alta189.cyborg.api.util.config.AbstractConfiguration; import com.alta189.cyborg.api.util.config.ConfigurationNode; import com.alta189.cyborg.api.util.config.FileConfiguration; import com.alta189.cyborg.api.util.config.commented.CommentedConfiguration; import com.alta189.cyborg.api.util.config.commented.CommentedConfigurationNode; import org.apache.commons.lang3.ArrayUtils; import org.apache.commons.lang3.StringUtils; import java.io.BufferedReader; import java.io.BufferedWriter; import java.io.File; import java.io.FileInputStream; import java.io.FileOutputStream; import java.io.IOException; import java.io.InputStreamReader; import java.io.OutputStreamWriter; 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 static com.alta189.cyborg.api.util.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 * <p/> * 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> * <p/> * 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 File file; public IniConfiguration(File file) { this.file = file; } 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; } 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 * @param value * @return */ 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; } } public CommentedConfigurationNode getNode(String path) { return (CommentedConfigurationNode) super.getNode(path); } 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; } else { 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); } protected Reader getReader() throws IOException { return new InputStreamReader(new FileInputStream(file), "UTF-8"); } protected Writer getWriter() throws IOException { return new OutputStreamWriter(new FileOutputStream(file), "UTF-8"); } public File getFile() { return file; } 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; } }