/* This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ package pluginbase.bukkit.properties; import pluginbase.logging.Logging; import pluginbase.messages.PluginBaseException; import org.bukkit.configuration.InvalidConfigurationException; import org.bukkit.configuration.file.YamlConfiguration; import org.jetbrains.annotations.NotNull; import java.io.BufferedReader; import java.io.File; import java.io.FileInputStream; import java.io.FileNotFoundException; import java.io.FileOutputStream; import java.io.IOException; import java.io.InputStream; import java.io.InputStreamReader; import java.io.OutputStreamWriter; import java.io.Reader; import java.io.StringWriter; import java.io.UnsupportedEncodingException; import java.io.Writer; import java.util.HashMap; import java.util.List; /** * A Configuration wrapper class that allows for comments to be applied to the config paths. */ class CommentedYamlConfiguration extends YamlConfiguration implements CommentedFile { private final HashMap<String, String> comments = new HashMap<String, String>();; private final boolean doComments; CommentedYamlConfiguration(final boolean doComments) { this.doComments = doComments; } static CommentedYamlConfiguration loadCommentedConfiguration(@NotNull final File file, final boolean doComments) throws PluginBaseException { CommentedYamlConfiguration config; try { config = new EncodedYamlConfiguration("UTF-8", doComments); } catch (UnsupportedEncodingException e) { Logging.warning("Could not create UTF-8 configuration. Special/Foreign characters may not be saved."); config = new CommentedYamlConfiguration(doComments); } try { config.load(file); } catch (FileNotFoundException e) { throw new PluginBaseException(e); } catch (IOException e) { throw new PluginBaseException(e); } catch (InvalidConfigurationException e) { throw new PluginBaseException(e); } return config; } /** * Saves the file as per normal for YamlConfiguration and then parses the file and inserts * comments where necessary. */ private void _save(@NotNull final File file) throws IOException { // if there's comments to add and it saved fine, we need to add comments if (doComments && !comments.isEmpty()) { // String array of each line in the config file String[] yamlContents = this.convertFileToString(file).split("[" + System.getProperty("line.separator") + "]"); String header = options().header(); if (header == null) { header = ""; } // This will hold the entire newly formatted config StringBuilder newContents = new StringBuilder(header).append(System.getProperty("line.separator")).append(System.getProperty("line.separator")); // This holds the current path the lines are at in the config StringBuilder currentPath = new StringBuilder(); // This tells if the specified path has already been commented boolean commentedPath = false; // This flags if the line is a node or unknown text. boolean node = false; // The depth of the path. (number of words separated by periods - 1) int depth = 0; // TODO find a better solution here? // This will cause the first line to be ignored. boolean firstLine = true; // Loop through the config lines for (final String line : yamlContents) { if (firstLine) { firstLine = false; if (line.startsWith("#")) { continue; } } // If the line is a node (and not something like a list value) if (line.contains(": ") || (line.length() > 1 && line.charAt(line.length() - 1) == ':')) { // This is a new node so we need to mark it for commenting (if there are comments) commentedPath = false; // This is a node so flag it as one node = true; // Grab the index of the end of the node name int index = 0; index = line.indexOf(": "); if (index < 0) { index = line.length() - 1; } // If currentPath is empty, store the node name as the currentPath. (this is only on the first iteration, i think) if (currentPath.toString().isEmpty()) { currentPath = new StringBuilder(line.substring(0, index)); } else { // Calculate the whitespace preceding the node name int whiteSpace = 0; for (int n = 0; n < line.length(); n++) { if (line.charAt(n) == ' ') { whiteSpace++; } else { break; } } // Find out if the current depth (whitespace * 2) is greater/lesser/equal to the previous depth if (whiteSpace / 2 > depth) { // Path is deeper. Add a . and the node name currentPath.append(".").append(line.substring(whiteSpace, index)); depth++; } else if (whiteSpace / 2 < depth) { // Path is shallower, calculate current depth from whitespace (whitespace / 2) and subtract that many levels from the currentPath int newDepth = whiteSpace / 2; for (int i = 0; i < depth - newDepth; i++) { currentPath.replace(currentPath.lastIndexOf("."), currentPath.length(), ""); } // Grab the index of the final period int lastIndex = currentPath.lastIndexOf("."); if (lastIndex < 0) { // if there isn't a final period, set the current path to nothing because we're at root currentPath = new StringBuilder(); } else { // If there is a final period, replace everything after it with nothing currentPath.replace(currentPath.lastIndexOf("."), currentPath.length(), "").append("."); } // Add the new node name to the path currentPath.append(line.substring(whiteSpace, index)); // Reset the depth depth = newDepth; } else { // Path is same depth, replace the last path node name to the current node name int lastIndex = currentPath.lastIndexOf("."); if (lastIndex < 0) { // if there isn't a final period, set the current path to nothing because we're at root currentPath = new StringBuilder(); } else { // If there is a final period, replace everything after it with nothing currentPath.replace(currentPath.lastIndexOf("."), currentPath.length(), "").append("."); } //currentPath = currentPath.replace(currentPath.substring(currentPath.lastIndexOf(".")), ""); currentPath.append(line.substring(whiteSpace, index)); } } } else { node = false; } StringBuilder newLine = new StringBuilder(line); if (node) { String comment = null; if (!commentedPath) { // If there's a comment for the current path, retrieve it and flag that path as already commented comment = comments.get(currentPath.toString()); } if (comment != null && !comment.isEmpty()) { // Add the comment to the beginning of the current line newLine.insert(0, System.getProperty("line.separator")).insert(0, comment); comment = null; commentedPath = true; } /* Old code for removing uncommented lines. * May need reworking. if (comment != null || (line.length() > 1 && line.charAt(line.length() - 1) == ':')) { // Add the (modified) line to the total config String // This modified version will not write the config if a comment is not present newContents += line + System.getProperty("line.separator"); } */ } newLine.append(System.getProperty("line.separator")); // Add the (modified) line to the total config String newContents.append(newLine.toString()); } /* * Due to a bukkit bug we need to strip any extra new lines from the * beginning of this file, else they will multiply. */ /* while (newContents.startsWith(System.getProperty("line.separator"))) { newContents = newContents.replaceFirst(System.getProperty("line.separator"), ""); } */ // Write the string to the config file this.stringToFile(newContents.toString(), file); } } /** * Adds a comment just before the specified path. The comment can be multiple lines. An empty string will indicate a blank line. * * @param path Configuration path to add comment. * @param commentLines Comments to add. One String per line. */ public void addComments(@NotNull final String path, @NotNull final List<String> commentLines) { StringBuilder commentstring = new StringBuilder(); String leadingSpaces = ""; for (int n = 0; n < path.length(); n++) { if (path.charAt(n) == '.') { leadingSpaces += " "; } } for (String line : commentLines) { if (!line.isEmpty()) { if (line.charAt(0) != '#') { line = "# " + line; } line = leadingSpaces + line; } else { line = " "; } if (commentstring.length() > 0) { commentstring.append("\r\n"); } commentstring.append(line); } comments.put(path, commentstring.toString()); } /** * Pass a file and it will return it's contents as a string. * * @param file File to read. * @return Contents of file. String will be empty in case of any errors. */ private String convertFileToString(File file) { final int bufferSize = 1024; if (file != null && file.exists() && file.canRead() && !file.isDirectory()) { Writer writer = new StringWriter(); InputStream is = null; char[] buffer = new char[bufferSize]; try { is = new FileInputStream(file); Reader reader = new BufferedReader( new InputStreamReader(is, "UTF-8")); int n; while ((n = reader.read(buffer)) != -1) { writer.write(buffer, 0, n); } } catch (IOException e) { e.printStackTrace(); } finally { if (is != null) { try { is.close(); } catch (IOException ignore) { } } } return writer.toString(); } else { return ""; } } /** * Writes the contents of a string to a file. * * @param source String to write. * @param file File to write to. * @return True on success. * @throws java.io.IOException */ private boolean stringToFile(String source, File file) throws IOException { OutputStreamWriter out = null; try { out = new OutputStreamWriter(new FileOutputStream(file), "UTF-8"); source.replaceAll("\n", System.getProperty("line.separator")); out.write(source); out.close(); return true; } catch (IOException e) { e.printStackTrace(); return false; } finally { if (out != null) { try { out.close(); } catch (IOException e) { e.printStackTrace(); } } } } @Override public void save(@NotNull final File file) throws IOException { super.save(file); _save(file); } }