/** * This file is part of Skript. * * Skript 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. * * Skript 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 Skript. If not, see <http://www.gnu.org/licenses/>. * * * Copyright 2011-2017 Peter Güttinger and contributors */ package ch.njol.skript.config; import java.io.ByteArrayInputStream; import java.io.File; import java.io.FileInputStream; import java.io.IOException; import java.io.InputStream; import java.io.PrintWriter; import java.lang.reflect.Field; import java.lang.reflect.Modifier; import java.nio.channels.Channels; import java.nio.channels.FileChannel; import java.nio.file.Path; import java.util.HashMap; import org.eclipse.jdt.annotation.NonNull; import org.eclipse.jdt.annotation.Nullable; import ch.njol.skript.Skript; import ch.njol.skript.config.validate.SectionValidator; /** * Represents a config file. * * @author Peter Güttinger */ public class Config implements Comparable<Config> { boolean simple = false; /** * One level of the indentation, e.g. a tab or 4 spaces. */ private String indentation = "\t"; /** * The indentation's name, i.e. 'tab' or 'space'. */ private String indentationName = "tab"; final String defaultSeparator; String separator; String line = ""; int level = 0; private final SectionNode main; int errors = 0; final boolean allowEmptySections; String fileName; @Nullable Path file = null; public Config(final InputStream source, final String fileName, @Nullable final File file, final boolean simple, final boolean allowEmptySections, final String defaultSeparator) throws IOException { try { this.fileName = fileName; if (file != null) // Must check for null before converting to path this.file = file.toPath(); this.simple = simple; this.allowEmptySections = allowEmptySections; this.defaultSeparator = defaultSeparator; separator = defaultSeparator; if (source.available() == 0) { main = new SectionNode(this); Skript.warning("'" + getFileName() + "' is empty"); return; } if (Skript.logVeryHigh()) Skript.info("loading '" + fileName + "'"); final ConfigReader r = new ConfigReader(source); try { main = SectionNode.load(this, r); } finally { r.close(); } } finally { source.close(); } } public Config(final InputStream source, final String fileName, final boolean simple, final boolean allowEmptySections, final String defaultSeparator) throws IOException { this(source, fileName, null, simple, allowEmptySections, defaultSeparator); } @SuppressWarnings("resource") public Config(final File file, final boolean simple, final boolean allowEmptySections, final String defaultSeparator) throws IOException { this(new FileInputStream(file), "" + file.getName(), simple, allowEmptySections, defaultSeparator); this.file = file.toPath(); } @SuppressWarnings("null") public Config(final Path file, final boolean simple, final boolean allowEmptySections, final String defaultSeparator) throws IOException { this(Channels.newInputStream(FileChannel.open(file)), "" + file.getFileName(), simple, allowEmptySections, defaultSeparator); this.file = file; } /** * For testing * * @param s * @param fileName * @param simple * @param allowEmptySections * @param defaultSeparator * @throws IOException */ public Config(final String s, final String fileName, final boolean simple, final boolean allowEmptySections, final String defaultSeparator) throws IOException { this(new ByteArrayInputStream(s.getBytes(ConfigReader.UTF_8)), fileName, simple, allowEmptySections, defaultSeparator); } void setIndentation(final String indent) { assert indent != null && indent.length() > 0 : indent; indentation = indent; indentationName = (indent.charAt(0) == ' ' ? "space" : "tab"); } String getIndentation() { return indentation; } String getIndentationName() { return indentationName; } public SectionNode getMainNode() { return main; } public String getFileName() { return fileName; } /** * Saves the config to a file. * * @param f The file to save to * @throws IOException If the file could not be written to. */ public void save(final File f) throws IOException { separator = defaultSeparator; final PrintWriter w = new PrintWriter(f, "UTF-8"); try { main.save(w); } finally { w.flush(); w.close(); } } /** * Sets this config's values to those in the given config. * <p> * Used by Skript to import old settings into the updated config. The return value is used to not modify the config if no new options were added. * * @param other * @return Whether the configs' keys differ, i.e. false == configs only differ in values, not keys. */ public boolean setValues(final Config other) { return getMainNode().setValues(other.getMainNode()); } public boolean setValues(final Config other, final String... excluded) { return getMainNode().setValues(other.getMainNode(), excluded); } @Nullable public File getFile() { if (file != null) { try { return file.toFile(); } catch(Exception e) { return null; // ZipPath, for example, throws undocumented exception } } return null; } @Nullable public Path getPath() { return file; } /** * @return The most recent separator. Only useful while the file is loading. */ public String getSeparator() { return separator; } /** * @return A separator string useful for saving, e.g. ": " or " = ". */ public String getSaveSeparator() { if (separator.equals(":")) return ": "; if (separator.equals("=")) return " = "; return " " + separator + " "; } /** * Splits the given path at the dot character and passes the result to {@link #get(String...)}. * * @param path * @return <tt>get(path.split("\\."))</tt> */ @SuppressWarnings("null") @Nullable public String getByPath(final String path) { return get(path.split("\\.")); } /** * Gets an entry node's value at the designated path * * @param path * @return The entry node's value at the location defined by path or null if it either doesn't exist or is not an entry. */ @Nullable public String get(final String... path) { SectionNode section = main; for (int i = 0; i < path.length; i++) { final Node n = section.get(path[i]); if (n == null) return null; if (n instanceof SectionNode) { if (i == path.length - 1) return null; section = (SectionNode) n; } else { if (n instanceof EntryNode && i == path.length - 1) return ((EntryNode) n).getValue(); else return null; } } return null; } public boolean isEmpty() { return main.isEmpty(); } public HashMap<String, String> toMap(final String separator) { return main.toMap("", separator); } public boolean validate(final SectionValidator validator) { return validator.validate(getMainNode()); } private void load(final Class<?> c, final @Nullable Object o, final String path) { for (final Field f : c.getDeclaredFields()) { f.setAccessible(true); if (o != null || Modifier.isStatic(f.getModifiers())) { try { if (OptionSection.class.isAssignableFrom(f.getType())) { final Object p = f.get(o); @SuppressWarnings("null") @NonNull final Class<?> pc = p.getClass(); load(pc, p, path + ((OptionSection) p).key + "."); } else if (Option.class.isAssignableFrom(f.getType())) { ((Option<?>) f.get(o)).set(this, path); } } catch (final IllegalArgumentException e) { assert false; } catch (final IllegalAccessException e) { assert false; } } } } /** * Sets all {@link Option} fields of the given object to the values from this config */ @SuppressWarnings("null") public void load(final Object o) { load(o.getClass(), o, ""); } /** * Sets all static {@link Option} fields of the given class to the values from this config */ public void load(final Class<?> c) { load(c, null, ""); } @Override public int compareTo(@Nullable Config other) { if (other == null) return 0; return fileName.compareTo(other.fileName); } }