/* * Copyright (c) 2006-2017 DMDirc Developers * * 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.dmdirc.util.io; import java.io.IOException; import java.io.InputStream; import java.nio.charset.Charset; import java.nio.file.Path; import java.util.ArrayList; import java.util.Collection; import java.util.Collections; import java.util.GregorianCalendar; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.stream.Collectors; /** * Reads and writes a standard DMDirc config file. */ public class ConfigFile extends TextFile { /** A list of domains in this config file. */ private final Collection domains = new ArrayList<>(); /** The values associated with each flat domain. */ private final Map> flatdomains = new HashMap<>(); /** The key/value sets associated with each key domain. */ private final Map> keydomains = new HashMap<>(); /** Whether or not we should automatically create domains. */ private boolean automake; /** * Creates a new read-only Config File from the specified input stream. * * @param is The input stream to read */ public ConfigFile(final InputStream is) { super(is, Charset.forName("UTF-8")); } /** * Creates a new config file from the specified path. * * @param path The path to read/write. */ public ConfigFile(final Path path) { super(path, Charset.forName("UTF-8")); } /** * Sets the "automake" value of this config file. If automake is set to * true, any calls to getKeyDomain will automatically create the domain * if it did not previously exist. * * @param automake The new value of the automake setting of this file */ public void setAutomake(final boolean automake) { this.automake = automake; } /** * Reads the data from the file. * * @throws IOException if an i/o exception occurred when reading * @throws InvalidConfigFileException if the config file isn't valid */ public void read() throws IOException, InvalidConfigFileException { keydomains.clear(); flatdomains.clear(); domains.clear(); readLines(); String domain = null; boolean keydomain = false; for (String line : getLines()) { String tline = line; while (!tline.isEmpty() && (tline.charAt(0) == '\t' || tline.charAt(0) == ' ')) { tline = tline.substring(1); } if (tline.indexOf('#') == 0 || tline.isEmpty()) { continue; } final int offset; if (tline.endsWith(":") && !tline.endsWith("\\:") && findEquals(tline) == -1) { domain = unescape(tline.substring(0, tline.length() - 1)); domains.add(domain); keydomain = keydomains.containsKey(domain) || hasFlatDomainValue("keysections", domain); if (keydomain && !keydomains.containsKey(domain)) { keydomains.put(domain, new HashMap<>()); } else if (!keydomain && !flatdomains.containsKey(domain)) { flatdomains.put(domain, new ArrayList<>()); } } else if (domain != null && keydomain && (offset = findEquals(tline)) != -1) { final String key = unescape(tline.substring(0, offset)); final String value = unescape(tline.substring(offset + 1)); keydomains.get(domain).put(key, value); } else if (domain != null && !keydomain) { addFlatDomainValue(domain, unescape(tline)); } else { throw new InvalidConfigFileException("Unknown or unexpected" + " line encountered: " + tline); } } } /** * Writes the contents of this ConfigFile to disk. * * @throws IOException if the write operation fails */ public void write() throws IOException { if (!isWritable()) { throw new UnsupportedOperationException("Cannot write to a file " + "that isn't writable"); } final Collection lines = new ArrayList<>(); lines.add("# This is a DMDirc configuration file."); lines.add("# Written on: " + new GregorianCalendar().getTime()); writeMeta(lines); for (String domain : domains) { if ("keysections".equals(domain)) { continue; } lines.add(""); lines.add(escape(domain) + ':'); if (flatdomains.containsKey(domain)) { lines.addAll(flatdomains.get(domain).stream() .map(entry -> " " + escape(entry)) .collect(Collectors.toList())); } else { lines.addAll(keydomains.get(domain).entrySet().stream() .map(entry -> " " + escape(entry.getKey()) + '=' + escape(entry.getValue())) .collect(Collectors.toList())); } } writeLines(lines); } /** * Appends the meta-data (keysections) to the specified list of lines. * * @param lines The set of lines to be appended to */ private void writeMeta(final Collection lines) { lines.add(""); lines.add("# This section indicates which sections below take " + "key/value"); lines.add("# pairs, rather than a simple list. It should be " + "placed above"); lines.add("# any sections that take key/values."); lines.add("keysections:"); lines.addAll(domains.stream() .filter(domain -> !"keysections".equals(domain) && keydomains.containsKey(domain)) .map(domain -> " " + domain) .collect(Collectors.toList())); } /** * Retrieves all the key domains for this config file. * * @return This config file's key domains */ public Map> getKeyDomains() { return Collections.unmodifiableMap(keydomains); } /** * Retrieves the key/values of the specified key domain. * * @param domain The domain to be retrieved * @return A map of keys to values in the specified domain */ public Map getKeyDomain(final String domain) { if (automake && !isKeyDomain(domain)) { domains.add(domain); keydomains.put(domain, new HashMap<>()); } return keydomains.get(domain); } /** * Retrieves the content of the specified flat domain. * * @param domain The domain to be retrieved * @return A list of lines in the specified domain */ public List getFlatDomain(final String domain) { return flatdomains.get(domain); } /** * Determines if this config file has the specified domain. * * @param domain The domain to check for * @return True if the domain is known, false otherwise */ public boolean hasDomain(final String domain) { return keydomains.containsKey(domain) || flatdomains.containsKey(domain); } /** * Determines if this config file has the specified domain, and the domain * is a key domain. * * @param domain The domain to check for * @return True if the domain is known and keyed, false otherwise */ public boolean isKeyDomain(final String domain) { return keydomains.containsKey(domain); } /** * Determines if this config file has the specified domain, and the domain * is a flat domain. * * @param domain The domain to check for * @return True if the domain is known and flat, false otherwise */ public boolean isFlatDomain(final String domain) { return flatdomains.containsKey(domain); } /** * Adds a new key domain to this config file. * * @param name The name of the domain to be added * @param data The content of the domain */ public void addDomain(final String name, final Map data) { domains.add(name); keydomains.put(name, data); } private boolean hasFlatDomainValue(final String domain, final String value) { return flatdomains.containsKey(domain) && flatdomains.get(domain).contains(value); } private void addFlatDomainValue(final String domain, final String value) { if (!flatdomains.containsKey(domain)) { flatdomains.put(domain, new ArrayList<>()); } flatdomains.get(domain).add(value); } /** * Unescapes any escaped characters in the specified input string. * * @param input The string to unescape * @return The string with all escape chars (\) resolved */ protected static String unescape(final CharSequence input) { boolean escaped = false; final StringBuilder temp = new StringBuilder(); for (int i = 0; i < input.length(); i++) { final char ch = input.charAt(i); if (escaped) { if (ch == 'n') { temp.append('\n'); } else if (ch == 'r') { temp.append('\r'); } else { temp.append(ch); } escaped = false; } else if (ch == '\\') { escaped = true; } else { temp.append(ch); } } return temp.toString(); } /** * Escapes the specified input string by prefixing all occurances of * \, \n, \r, =, # and : with backslashes. * * @param input The string to be escaped * @return A backslash-armoured version of the string */ protected static String escape(final String input) { return input.replace("\\", "\\\\").replace("\n", "\\n") .replace("\r", "\\r").replace("=", "\\=") .replace(":", "\\:").replace("#", "\\#"); } /** * Finds the first non-escaped instance of '=' in the specified string. * * @param input The string to be searched * @return The offset of the first non-escaped instance of '=', or -1. */ private static int findEquals(final CharSequence input) { boolean escaped = false; for (int i = 0; i < input.length(); i++) { if (escaped) { escaped = false; } else if (input.charAt(i) == '\\') { escaped = true; } else if (input.charAt(i) == '=') { return i; } } return -1; } }