/* * 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-2014 Peter Güttinger * */ package ch.njol.skript.config; import java.io.IOException; import java.io.PrintWriter; import java.util.ArrayList; import java.util.HashMap; import java.util.Iterator; import org.eclipse.jdt.annotation.NonNull; import org.eclipse.jdt.annotation.Nullable; import ch.njol.skript.Skript; import ch.njol.skript.SkriptAPIException; import ch.njol.skript.config.validate.EntryValidator; import ch.njol.skript.config.validate.SectionValidator; import ch.njol.skript.log.SkriptLogger; import ch.njol.util.NonNullPair; import ch.njol.util.NullableChecker; import ch.njol.util.coll.CollectionUtils; import ch.njol.util.coll.iterator.CheckedIterator; /** * @author Peter Güttinger */ public class SectionNode extends Node implements Iterable<Node> { private final ArrayList<Node> nodes = new ArrayList<Node>(); public SectionNode(final String key, final String comment, final SectionNode parent, final int lineNum) { super(key, comment, parent, lineNum); } SectionNode(final Config c) { super(c); } /** * Note to self: use getNodeMap() */ @Nullable private NodeMap nodeMap = null; private NodeMap getNodeMap() { NodeMap nodeMap = this.nodeMap; if (nodeMap == null) { nodeMap = this.nodeMap = new NodeMap(); for (final Node node : nodes) { assert node != null; nodeMap.put(node); } } return nodeMap; } /** * @return Total amount of nodes (including void nodes) in this section. */ public int size() { return nodes.size(); } /** * Adds the given node at the end of this section. * * @param n */ public void add(final Node n) { n.remove(); nodes.add(n); n.parent = this; n.config = config; getNodeMap().put(n); } /** * Inserts the given node into this section at the specified position. * * @param n * @param index between 0 and {@link #size()}, inclusive */ public void insert(final Node n, final int index) { nodes.add(index, n); n.parent = this; n.config = config; getNodeMap().put(n); } /** * Removes the given node from this section. * * @param n */ public void remove(final Node n) { nodes.remove(n); n.parent = null; getNodeMap().remove(n); } /** * Removes an entry with the given key. * * @param key * @return The removed node, or null if the key didn't match any node. */ @Nullable public Node remove(final String key) { final Node n = getNodeMap().remove(key); if (n == null) return null; nodes.remove(n); n.parent = null; return n; } /** * Iterator over all non-void nodes of this section. */ @Override public Iterator<Node> iterator() { @SuppressWarnings("null") @NonNull final Iterator<Node> iter = nodes.iterator(); return new CheckedIterator<Node>(iter, new NullableChecker<Node>() { @Override public boolean check(final @Nullable Node n) { return n != null && !n.isVoid(); } }) { @Override public boolean hasNext() { final boolean hasNext = super.hasNext(); if (!hasNext) SkriptLogger.setNode(SectionNode.this); return hasNext; } @Override @Nullable public Node next() { final Node n = super.next(); SkriptLogger.setNode(n); return n; } @Override public void remove() { throw new UnsupportedOperationException(); } }; } /** * Gets a subnode (EntryNode or SectionNode) with the specified name. * * @param key * @return The node with the given name */ @Nullable public Node get(final @Nullable String key) { return getNodeMap().get(key); } @Nullable public String getValue(final String key) { final Node n = get(key); if (n instanceof EntryNode) return ((EntryNode) n).getValue(); return null; } /** * Gets an entry's value or the default value if it doesn't exist or is not an EntryNode. * * @param name The name of the node (case insensitive) * @param def The default value * @return The value of the entry node with the give node, or <tt>def</tt> if there's no entry with the given name. */ public String get(final String name, final String def) { final Node n = this.get(name); if (n == null || !(n instanceof EntryNode)) return def; return ((EntryNode) n).getValue(); } public void set(final String key, final String value) { final Node n = get(key); if (n instanceof EntryNode) { ((EntryNode) n).setValue(value); } else { add(new EntryNode(key, value, this)); } } public void set(final String key, final @Nullable Node node) { if (node == null) { remove(key); return; } final Node n = get(key); if (n != null) { for (int i = 0; i < nodes.size(); i++) { if (nodes.get(i) == n) { nodes.set(i, node); remove(n); getNodeMap().put(node); node.parent = this; node.config = config; return; } } assert false; } add(node); } void renamed(final Node node, final @Nullable String oldKey) { if (!nodes.contains(node)) throw new IllegalArgumentException(); getNodeMap().remove(oldKey); getNodeMap().put(node); } public boolean isEmpty() { for (final Node node : nodes) { if (!node.isVoid()) return false; } return true; } final static SectionNode load(final Config c, final ConfigReader r) throws IOException { return new SectionNode(c).load_i(r); } final static SectionNode load(final String name, final String comment, final SectionNode parent, final ConfigReader r) throws IOException { parent.config.level++; final SectionNode node = new SectionNode(name, comment, parent, r.getLineNum()).load_i(r); SkriptLogger.setNode(parent); parent.config.level--; return node; } private final static String readableWhitespace(final String s) { if (s.matches(" +")) return s.length() + " space" + (s.length() == 1 ? "" : "s"); if (s.matches("\t+")) return s.length() + " tab" + (s.length() == 1 ? "" : "s"); return "'" + s.replace("\t", "->").replace(' ', '_').replaceAll("\\s", "?") + "' [-> = tab, _ = space, ? = other whitespace]"; } private final SectionNode load_i(final ConfigReader r) throws IOException { boolean indentationSet = false; String fullLine; while ((fullLine = r.readLine()) != null) { SkriptLogger.setNode(this); final NonNullPair<String, String> line = Node.splitLine(fullLine); String value = line.getFirst(); final String comment = line.getSecond(); final SectionNode parent = this.parent; if (!indentationSet && parent != null && parent.parent == null && !value.isEmpty() && !value.matches("\\s*") && !value.matches("\\S.*")) { final String s = value.replaceFirst("\\S.*$", ""); assert !s.isEmpty() : fullLine; if (s.matches(" +") || s.matches("\t+")) { config.setIndentation(s); indentationSet = true; } else { nodes.add(new InvalidNode(value, comment, this, r.getLineNum())); Skript.error("indentation error: indent must only consist of either spaces or tabs, but not mixed (found " + readableWhitespace(s) + ")"); continue; } } if (!value.matches("\\s*") && !value.matches("^(" + config.getIndentation() + "){" + config.level + "}\\S.*")) { if (value.matches("^(" + config.getIndentation() + "){" + config.level + "}\\s.*") || !value.matches("^(" + config.getIndentation() + ")*\\S.*")) { nodes.add(new InvalidNode(value, comment, this, r.getLineNum())); final String s = "" + value.replaceFirst("\\S.*$", ""); Skript.error("indentation error: expected " + config.level * config.getIndentation().length() + " " + config.getIndentationName() + (config.level * config.getIndentation().length() == 1 ? "" : "s") + ", but found " + readableWhitespace(s)); continue; } else { if (parent != null && !config.allowEmptySections && isEmpty()) { Skript.warning("Empty configuration section! You might want to indent one or more of the subsequent lines to make them belong to this section" + " or remove the colon at the end of the line if you don't want this line to start a section."); } r.reset(); return this; } } value = value.trim(); if (value.isEmpty()) { nodes.add(new VoidNode(value, comment, this, r.getLineNum())); continue; } // if (line.startsWith("!") && line.indexOf('[') != -1 && line.endsWith("]")) { // final String option = line.substring(1, line.indexOf('[')); // final String value = line.substring(line.indexOf('[') + 1, line.length() - 1); // if (value.isEmpty()) { // nodes.add(new InvalidNode(this, r)); // Skript.error("parse options must not be empty"); // continue; // } else if (option.equalsIgnoreCase("separator")) { // if (config.simple) { // Skript.warning("scripts don't have a separator"); // continue; // } // config.separator = value; // } else { // final Node n = new InvalidNode(this, r); // SkriptLogger.setNode(n); // nodes.add(n); // Skript.error("unknown parse option '" + option + "'"); // continue; // } // nodes.add(new ParseOptionNode(line.substring(0, line.indexOf('[')), this, r)); // continue; // } if (value.endsWith(":") && (config.simple || value.indexOf(config.separator) == -1 || config.separator.endsWith(":") && value.indexOf(config.separator) == value.length() - config.separator.length() ) && !fullLine.matches("([^#]|##)*#-#(\\s.*)?")) { nodes.add(SectionNode.load("" + value.substring(0, value.length() - 1), comment, this, r)); continue; } if (config.simple) { nodes.add(new SimpleNode(value, comment, r.getLineNum(), this)); } else { nodes.add(getEntry(value, comment, r.getLineNum(), config.separator)); } } SkriptLogger.setNode(parent); return this; } private Node getEntry(final String keyAndValue, final String comment, final int lineNum, final String separator) { final int x = keyAndValue.indexOf(separator); if (x == -1) { final InvalidNode in = new InvalidNode(keyAndValue, comment, this, lineNum); EntryValidator.notAnEntryError(in); SkriptLogger.setNode(this); return in; } final String key = "" + keyAndValue.substring(0, x).trim(); final String value = "" + keyAndValue.substring(x + separator.length()).trim(); return new EntryNode(key, value, comment, this, lineNum); } /** * Converts all SimpleNodes in this section to EntryNodes. * * @param levels Amount of levels to go down, e.g. 0 to only convert direct subnodes of this section or -1 for all subnodes including subnodes of subnodes etc. */ public void convertToEntries(final int levels) { convertToEntries(levels, config.separator); } /** * REMIND breaks saving - separator argument can be different from config.sepator * * @param levels Maximum depth of recursion, <tt>-1</tt> for no limit. * @param separator Some separator, e.g. ":" or "=". */ public void convertToEntries(final int levels, final String separator) { if (levels < -1) throw new IllegalArgumentException("levels must be >= -1"); if (!config.simple) throw new SkriptAPIException("config is not simple: " + config); for (int i = 0; i < nodes.size(); i++) { final Node n = nodes.get(i); if (levels != 0 && n instanceof SectionNode) { ((SectionNode) n).convertToEntries(levels == -1 ? -1 : levels - 1, separator); } if (!(n instanceof SimpleNode)) continue; final String key = n.key; if (key != null) nodes.set(i, getEntry(key, n.comment, n.lineNum, separator)); else assert false; } } @Override public void save(final PrintWriter w) { if (parent != null) super.save(w); for (final Node node : nodes) node.save(w); } @Override String save_i() { assert key != null; return key + ":"; } public boolean validate(final SectionValidator validator) { return validator.validate(this); } HashMap<String, String> toMap(final String prefix, final String separator) { final HashMap<String, String> r = new HashMap<String, String>(); for (final Node n : this) { if (n instanceof EntryNode) { r.put(prefix + n.getKey(), ((EntryNode) n).getValue()); } else { r.putAll(((SectionNode) n).toMap(prefix + n.getKey() + separator, separator)); } } return r; } /** * @param other * @param excluded keys and sections to exclude * @return <tt>false</tt> iff this and the other SectionNode contain the exact same set of keys */ public boolean setValues(final SectionNode other, final String... excluded) { boolean r = false; for (final Node n : this) { if (CollectionUtils.containsIgnoreCase(excluded, n.key)) continue; final Node o = other.get(n.key); if (o == null) { r = true; } else { if (n instanceof SectionNode) { if (o instanceof SectionNode) { r |= ((SectionNode) n).setValues((SectionNode) o); } else { r = true; } } else if (n instanceof EntryNode) { if (o instanceof EntryNode) { ((EntryNode) n).setValue(((EntryNode) o).getValue()); } else { r = true; } } } } if (!r) { for (final Node o : other) { if (this.get(o.key) == null) { r = true; break; } } } return r; } }