getPlotAreas(String world) {
+ if (world == null) {
+ return Collections.emptySet();
+ }
+ return PlotSquared.get().getPlotAreas(world);
+ }
+
+ /**
+ * Send a message to the console. The message supports color codes.
+ *
+ * @param message the message
+ * @see MainUtil#sendConsoleMessage(C, String...)
+ */
+ public void sendConsoleMessage(String message) {
+ PlotSquared.log(message);
+ }
+
+ /**
+ * Send a message to the console.
+ *
+ * @param caption the message
+ * @see #sendConsoleMessage(String)
+ * @see C
+ */
+ public void sendConsoleMessage(C caption) {
+ sendConsoleMessage(caption.s());
+ }
+
+ /**
+ * Registers a flag for use in plots.
+ *
+ * @param flag the flag to register
+ */
+ public void addFlag(Flag> flag) {
+ Flags.registerFlag(flag);
+ }
+
+ /**
+ * Gets the PlotSquared class.
+ *
+ * @return PlotSquared Class
+ * @see PlotSquared
+ */
+ public PlotSquared getPlotSquared() {
+ return PlotSquared.get();
+ }
+
+ /**
+ * Get the PlotPlayer for a UUID.
+ *
+ * Please note that PlotSquared can be configured to provide
+ * different UUIDs than bukkit
+ *
+ * @param uuid the uuid of the player to wrap
+ * @return a {@code PlotPlayer}
+ * @see PlotPlayer#wrap(Object)
+ */
+ public PlotPlayer wrapPlayer(UUID uuid) {
+ return PlotPlayer.wrap(uuid);
+ }
+
+ /**
+ * Get the PlotPlayer for a username.
+ *
+ * @param player the player to wrap
+ * @return a {@code PlotPlayer}
+ * @see PlotPlayer#wrap(Object)
+ */
+ public PlotPlayer wrapPlayer(String player) {
+ return PlotPlayer.wrap(player);
+ }
+}
diff --git a/Bukkit/src/main/java/com/github/intellectualsites/plotsquared/commands/Argument.java b/Bukkit/src/main/java/com/github/intellectualsites/plotsquared/commands/Argument.java
new file mode 100644
index 000000000..dd5221a3a
--- /dev/null
+++ b/Bukkit/src/main/java/com/github/intellectualsites/plotsquared/commands/Argument.java
@@ -0,0 +1,68 @@
+package com.github.intellectualsites.plotsquared.commands;
+
+import com.github.intellectualsites.plotsquared.plot.object.PlotId;
+
+public abstract class Argument {
+
+ public static final Argument Integer = new Argument("int", 16) {
+ @Override public Integer parse(String in) {
+ Integer value = null;
+ try {
+ value = java.lang.Integer.parseInt(in);
+ } catch (Exception ignored) {
+ }
+ return value;
+ }
+ };
+ public static final Argument Boolean = new Argument("boolean", true) {
+ @Override public Boolean parse(String in) {
+ Boolean value = null;
+ if (in.equalsIgnoreCase("true") || in.equalsIgnoreCase("Yes") || in
+ .equalsIgnoreCase("1")) {
+ value = true;
+ } else if (in.equalsIgnoreCase("false") || in.equalsIgnoreCase("No") || in
+ .equalsIgnoreCase("0")) {
+ value = false;
+ }
+ return value;
+ }
+ };
+ public static final Argument String = new Argument("String", "Example") {
+ @Override public String parse(String in) {
+ return in;
+ }
+ };
+ public static final Argument PlayerName =
+ new Argument("PlayerName", "Dinnerbone") {
+ @Override public String parse(String in) {
+ return in.length() <= 16 ? in : null;
+ }
+ };
+ public static final Argument PlotID =
+ new Argument("PlotID", new PlotId(-6, 3)) {
+ @Override public PlotId parse(String in) {
+ return PlotId.fromString(in);
+ }
+ };
+ private final String name;
+ private final T example;
+
+ public Argument(String name, T example) {
+ this.name = name;
+ this.example = example;
+ }
+
+ public abstract T parse(String in);
+
+ @Override public final String toString() {
+ return this.getName();
+ }
+
+ public final String getName() {
+ return this.name;
+ }
+
+ public final T getExample() {
+ return this.example;
+ }
+}
diff --git a/Bukkit/src/main/java/com/github/intellectualsites/plotsquared/commands/Command.java b/Bukkit/src/main/java/com/github/intellectualsites/plotsquared/commands/Command.java
new file mode 100644
index 000000000..6bac09a26
--- /dev/null
+++ b/Bukkit/src/main/java/com/github/intellectualsites/plotsquared/commands/Command.java
@@ -0,0 +1,596 @@
+package com.github.intellectualsites.plotsquared.commands;
+
+import com.github.intellectualsites.plotsquared.configuration.file.YamlConfiguration;
+import com.github.intellectualsites.plotsquared.plot.PlotSquared;
+import com.github.intellectualsites.plotsquared.plot.commands.CommandCategory;
+import com.github.intellectualsites.plotsquared.plot.commands.MainCommand;
+import com.github.intellectualsites.plotsquared.plot.commands.RequiredType;
+import com.github.intellectualsites.plotsquared.plot.config.C;
+import com.github.intellectualsites.plotsquared.plot.object.PlotMessage;
+import com.github.intellectualsites.plotsquared.plot.object.PlotPlayer;
+import com.github.intellectualsites.plotsquared.plot.object.RunnableVal2;
+import com.github.intellectualsites.plotsquared.plot.object.RunnableVal3;
+import com.github.intellectualsites.plotsquared.plot.util.*;
+
+import java.io.IOException;
+import java.lang.annotation.Annotation;
+import java.lang.reflect.InvocationTargetException;
+import java.lang.reflect.Method;
+import java.util.*;
+
+public abstract class Command {
+
+ // May be none
+ private final ArrayList allCommands = new ArrayList<>();
+ private final ArrayList dynamicCommands = new ArrayList<>();
+ private final HashMap staticCommands = new HashMap<>();
+
+ // Parent command (may be null)
+ private final Command parent;
+ private final boolean isStatic;
+ // The command ID
+ private String id;
+ private List aliases;
+ private RequiredType required;
+ private String usage;
+ private String description;
+ private String perm;
+ private boolean confirmation;
+ private CommandCategory category;
+ private Argument[] arguments;
+
+ public Command(Command parent, boolean isStatic, String id, String perm, RequiredType required,
+ CommandCategory cat) {
+ this.parent = parent;
+ this.isStatic = isStatic;
+ this.id = id;
+ this.perm = perm;
+ this.required = required;
+ this.category = cat;
+ this.aliases = Arrays.asList(id);
+ if (this.parent != null) {
+ this.parent.register(this);
+ }
+ }
+
+ public Command(Command parent, boolean isStatic) {
+ this.parent = parent;
+ this.isStatic = isStatic;
+ Annotation cdAnnotation = getClass().getAnnotation(CommandDeclaration.class);
+ if (cdAnnotation != null) {
+ CommandDeclaration declaration = (CommandDeclaration) cdAnnotation;
+ init(declaration);
+ }
+ for (final Method method : getClass().getDeclaredMethods()) {
+ if (method.isAnnotationPresent(CommandDeclaration.class)) {
+ Class>[] types = method.getParameterTypes();
+ // final PlotPlayer player, String[] args, RunnableVal3 confirm, RunnableVal2
+ // whenDone
+ if (types.length == 5 && types[0] == Command.class && types[1] == PlotPlayer.class
+ && types[2] == String[].class && types[3] == RunnableVal3.class
+ && types[4] == RunnableVal2.class) {
+ Command tmp = new Command(this, true) {
+ @Override public void execute(PlotPlayer player, String[] args,
+ RunnableVal3 confirm,
+ RunnableVal2 whenDone) {
+ try {
+ method.invoke(Command.this, this, player, args, confirm, whenDone);
+ } catch (IllegalAccessException | InvocationTargetException e) {
+ e.printStackTrace();
+ }
+ }
+ };
+ tmp.init(method.getAnnotation(CommandDeclaration.class));
+ }
+ }
+ }
+ }
+
+ public Command getParent() {
+ return this.parent;
+ }
+
+ public String getId() {
+ return this.id;
+ }
+
+ public String getFullId() {
+ if (this.parent != null && this.parent.getParent() != null) {
+ return this.parent.getFullId() + "." + this.id;
+ }
+ return this.id;
+ }
+
+ public List getCommands(PlotPlayer player) {
+ List commands = new ArrayList<>();
+ for (Command cmd : this.allCommands) {
+ if (cmd.canExecute(player, false)) {
+ commands.add(cmd);
+ }
+ }
+ return commands;
+ }
+
+ public List getCommands(CommandCategory cat, PlotPlayer player) {
+ List commands = getCommands(player);
+ if (cat != null) {
+ Iterator iterator = commands.iterator();
+ while (iterator.hasNext()) {
+ if (iterator.next().category != cat) {
+ iterator.remove();
+ }
+ }
+ }
+ return commands;
+ }
+
+ public List getCommands() {
+ return this.allCommands;
+ }
+
+ public boolean hasConfirmation(PlotPlayer player) {
+ return this.confirmation && !player.hasPermission(getPermission() + ".confirm.bypass");
+ }
+
+ public List getAliases() {
+ return this.aliases;
+ }
+
+ public String getDescription() {
+ return this.description;
+ }
+
+ public RequiredType getRequiredType() {
+ return this.required;
+ }
+
+ public Argument[] getRequiredArguments() {
+ return this.arguments;
+ }
+
+ public void setRequiredArguments(Argument[] arguments) {
+ this.arguments = arguments;
+ }
+
+ public void init(CommandDeclaration declaration) {
+ this.id = declaration.command();
+ this.perm = declaration.permission();
+ this.required = declaration.requiredType();
+ this.category = declaration.category();
+ HashMap options = new HashMap<>();
+ List aliasOptions = new ArrayList<>();
+ aliasOptions.add(this.id);
+ aliasOptions.addAll(Arrays.asList(declaration.aliases()));
+ options.put("aliases", aliasOptions);
+ options.put("description", declaration.description());
+ options.put("usage", declaration.usage());
+ options.put("confirmation", declaration.confirmation());
+ boolean set = false;
+ YamlConfiguration commands =
+ PlotSquared.get() == null ? new YamlConfiguration() : PlotSquared.get().commands;
+ for (Map.Entry entry : options.entrySet()) {
+ String key = this.getFullId() + "." + entry.getKey();
+ if (!commands.contains(key)) {
+ commands.set(key, entry.getValue());
+ set = true;
+ }
+ }
+ if (set && PlotSquared.get() != null) {
+ try {
+ commands.save(PlotSquared.get().commandsFile);
+ } catch (IOException e) {
+ e.printStackTrace();
+
+ }
+ }
+ this.aliases = commands.getStringList(this.getFullId() + ".aliases");
+ this.description = commands.getString(this.getFullId() + ".description");
+ this.usage = commands.getString(this.getFullId() + ".usage");
+ this.confirmation = commands.getBoolean(this.getFullId() + ".confirmation");
+ if (this.parent != null) {
+ this.parent.register(this);
+ }
+ }
+
+ public void register(Command command) {
+ if (command.isStatic) {
+ for (String alias : command.aliases) {
+ this.staticCommands.put(alias.toLowerCase(), command);
+ }
+ } else {
+ this.dynamicCommands.add(command);
+ }
+ this.allCommands.add(command);
+ }
+
+ public String getPermission() {
+ if (this.perm != null && !this.perm.isEmpty()) {
+ return this.perm;
+ }
+ if (this.parent == null) {
+ return "plots.use";
+ }
+ return "plots." + getFullId();
+ }
+
+ public void paginate(PlotPlayer player, List c, int size, int page,
+ RunnableVal3 add, String baseCommand, String header) {
+ // Calculate pages & index
+ if (page < 0) {
+ page = 0;
+ }
+ int totalPages = (int) Math.ceil(c.size() / size);
+ if (page > totalPages) {
+ page = totalPages;
+ }
+ int max = page * size + size;
+ if (max > c.size()) {
+ max = c.size();
+ }
+ // Send the header
+ header = header.replaceAll("%cur", page + 1 + "").replaceAll("%max", totalPages + 1 + "")
+ .replaceAll("%amount%", c.size() + "").replaceAll("%word%", "all");
+ MainUtil.sendMessage(player, header);
+ // Send the page content
+ List subList = c.subList(page * size, max);
+ int i = page * size;
+ for (T obj : subList) {
+ i++;
+ PlotMessage msg = new PlotMessage();
+ add.run(i, obj, msg);
+ msg.send(player);
+ }
+ // Send the footer
+ if (page < totalPages && page > 0) { // Back | Next
+ new PlotMessage().text("<-").color("$1").command(baseCommand + " " + page).text(" | ")
+ .color("$3").text("->").color("$1").command(baseCommand + " " + (page + 2))
+ .text(C.CLICKABLE.s()).color("$2").send(player);
+ return;
+ }
+ if (page == 0 && totalPages != 0) { // Next
+ new PlotMessage().text("<-").color("$3").text(" | ").color("$3").text("->").color("$1")
+ .command(baseCommand + " " + (0 + 2)).text(C.CLICKABLE.s()).color("$2")
+ .send(player);
+ return;
+ }
+ if (page == totalPages && totalPages != 0) { // Back
+ new PlotMessage().text("<-").color("$1").command(baseCommand + " " + page).text(" | ")
+ .color("$3").text("->").color("$3").text(C.CLICKABLE.s()).color("$2").send(player);
+ }
+ }
+
+ /**
+ * @param player Caller
+ * @param args Arguments
+ * @param confirm Instance, Success, Failure
+ * @return
+ */
+ public void execute(PlotPlayer player, String[] args,
+ RunnableVal3 confirm,
+ RunnableVal2 whenDone) throws CommandException {
+ if (args.length == 0 || args[0] == null) {
+ if (this.parent == null) {
+ MainCommand.getInstance().help.displayHelp(player, null, 0);
+ } else {
+ C.COMMAND_SYNTAX.send(player, getUsage());
+ }
+ return;
+ }
+ if (this.allCommands == null || this.allCommands.isEmpty()) {
+ player.sendMessage(
+ "Not Implemented: https://github.com/IntellectualSites/PlotSquared/issues/new");
+ return;
+ }
+ Command cmd = getCommand(args[0]);
+ if (cmd == null) {
+ if (this.parent != null) {
+ C.COMMAND_SYNTAX.send(player, getUsage());
+ return;
+ }
+ // Help command
+ try {
+ if (args.length == 0 || MathMan.isInteger(args[0])
+ || CommandCategory.valueOf(args[0].toUpperCase()) != null) {
+ // This will default certain syntax to the help command
+ // e.g. /plot, /plot 1, /plot claiming
+ MainCommand.getInstance().help.execute(player, args, null, null);
+ return;
+ }
+ } catch (IllegalArgumentException ignored) {
+ }
+ // Command recommendation
+ MainUtil.sendMessage(player, C.NOT_VALID_SUBCOMMAND);
+ List commands = getCommands(player);
+ if (commands.isEmpty()) {
+ MainUtil
+ .sendMessage(player, C.DID_YOU_MEAN, MainCommand.getInstance().help.getUsage());
+ return;
+ }
+ HashSet setargs = new HashSet<>(args.length);
+ for (String arg : args) {
+ setargs.add(arg.toLowerCase());
+ }
+ String[] allargs = setargs.toArray(new String[setargs.size()]);
+ int best = 0;
+ for (Command current : commands) {
+ int match = getMatch(allargs, current);
+ if (match > best) {
+ cmd = current;
+ }
+ }
+ if (cmd == null) {
+ cmd = new StringComparison<>(args[0], this.allCommands).getMatchObject();
+ }
+ MainUtil.sendMessage(player, C.DID_YOU_MEAN, cmd.getUsage());
+ return;
+ }
+ String[] newArgs = Arrays.copyOfRange(args, 1, args.length);
+ if (!cmd.checkArgs(player, newArgs) || !cmd.canExecute(player, true)) {
+ return;
+ }
+ try {
+ cmd.execute(player, newArgs, confirm, whenDone);
+ } catch (CommandException e) {
+ e.perform(player);
+ }
+ }
+
+ public boolean checkArgs(PlotPlayer player, String[] args) {
+ Argument>[] reqArgs = getRequiredArguments();
+ if (reqArgs != null && reqArgs.length > 0) {
+ boolean failed = args.length < reqArgs.length;
+ String[] baseSplit = getCommandString().split(" ");
+ String[] fullSplit = getUsage().split(" ");
+ String base = getCommandString();
+ if (fullSplit.length - baseSplit.length < reqArgs.length) {
+ String[] tmp = new String[baseSplit.length + reqArgs.length];
+ System.arraycopy(fullSplit, 0, tmp, 0, fullSplit.length);
+ fullSplit = tmp;
+ }
+ for (int i = 0; i < reqArgs.length; i++) {
+ fullSplit[i + baseSplit.length] = reqArgs[i].getExample().toString();
+ failed = failed || reqArgs[i].parse(args[i]) == null;
+ }
+ if (failed) {
+ C.COMMAND_SYNTAX.send(player, StringMan.join(fullSplit, " "));
+ return false;
+ }
+ }
+ return true;
+ }
+
+ public int getMatch(String[] args, Command cmd) {
+ int count = 0;
+ String perm = cmd.getPermission();
+ HashSet desc = new HashSet<>();
+ for (String alias : cmd.getAliases()) {
+ if (alias.startsWith(args[0])) {
+ count += 5;
+ }
+ }
+ Collections.addAll(desc, cmd.getDescription().split(" "));
+ for (String arg : args) {
+ if (perm.startsWith(arg)) {
+ count++;
+ }
+ if (desc.contains(arg)) {
+ count++;
+ }
+ }
+ String[] usage = cmd.getUsage().split(" ");
+ for (int i = 0; i < Math.min(4, usage.length); i++) {
+ int require;
+ if (usage[i].startsWith("<")) {
+ require = 1;
+ } else {
+ require = 0;
+ }
+ String[] split = usage[i].split("\\|| |\\>|\\<|\\[|\\]|\\{|\\}|\\_|\\/");
+ for (String aSplit : split) {
+ for (String arg : args) {
+ if (StringMan.isEqualIgnoreCase(arg, aSplit)) {
+ count += 5 - i + require;
+ }
+ }
+ }
+ }
+ count += StringMan.intersection(desc, args);
+ return count;
+ }
+
+ public Command getCommand(String arg) {
+ Command cmd = this.staticCommands.get(arg.toLowerCase());
+ if (cmd == null) {
+ for (Command command : this.dynamicCommands) {
+ if (command.matches(arg)) {
+ return command;
+ }
+ }
+ }
+ return cmd;
+ }
+
+ public Command getCommand(Class clazz) {
+ for (Command cmd : this.allCommands) {
+ if (cmd.getClass() == clazz) {
+ return cmd;
+ }
+ }
+ return null;
+ }
+
+ public Command getCommandById(String id) {
+ Command exact = this.staticCommands.get(id);
+ if (exact != null) {
+ return exact;
+ }
+ for (Command cmd : this.allCommands) {
+ if (cmd.getId().equals(id)) {
+ return cmd;
+ }
+ }
+ return null;
+ }
+
+ public boolean canExecute(PlotPlayer player, boolean message) {
+ if (player == null) {
+ return true;
+ }
+ if (!this.required.allows(player)) {
+ if (message) {
+ MainUtil.sendMessage(player,
+ this.required == RequiredType.PLAYER ? C.IS_CONSOLE : C.NOT_CONSOLE);
+ }
+ } else if (!Permissions.hasPermission(player, getPermission())) {
+ if (message) {
+ C.NO_PERMISSION.send(player, getPermission());
+ }
+ } else {
+ return true;
+ }
+ return false;
+ }
+
+ public boolean matches(String arg) {
+ arg = arg.toLowerCase();
+ return StringMan.isEqual(arg, this.id) || this.aliases.contains(arg);
+ }
+
+ public String getCommandString() {
+ String base;
+ if (this.parent == null) {
+ return "/" + toString();
+ } else {
+ return this.parent.getCommandString() + " " + toString();
+ }
+ }
+
+ public String getUsage() {
+ if (this.usage != null && !this.usage.isEmpty()) {
+ if (this.usage.startsWith("/")) {
+ return this.usage;
+ }
+ return getCommandString() + " " + this.usage;
+ }
+ if (this.allCommands.isEmpty()) {
+ return getCommandString();
+ }
+ StringBuilder args = new StringBuilder("[");
+ String prefix = "";
+ for (Command cmd : this.allCommands) {
+ args.append(prefix).append(cmd.isStatic ? cmd.toString() : "<" + cmd + ">");
+ prefix = "|";
+ }
+ return getCommandString() + " " + args + "]";
+ }
+
+ public Collection tabOf(PlotPlayer player, String[] input, boolean space,
+ String... args) {
+ if (!space) {
+ return null;
+ }
+ List result = new ArrayList<>();
+ int index = input.length - (space ? 0 : 1);
+ for (String arg : args) {
+ arg = arg.replace(getCommandString() + " ", "");
+ String[] split = arg.split(" ");
+ if (split.length <= index) {
+ continue;
+ }
+ arg = StringMan.join(Arrays.copyOfRange(split, index, split.length), " ");
+ Command cmd = new Command(null, false, arg, getPermission(), getRequiredType(), null) {
+ };
+ result.add(cmd);
+ }
+ return result;
+ }
+
+ public Collection tab(PlotPlayer player, String[] args, boolean space) {
+ switch (args.length) {
+ case 0:
+ return this.allCommands;
+ case 1:
+ String arg = args[0].toLowerCase();
+ if (space) {
+ Command cmd = getCommand(arg);
+ if (cmd != null && cmd.canExecute(player, false)) {
+ return cmd.tab(player, Arrays.copyOfRange(args, 1, args.length), space);
+ } else {
+ return null;
+ }
+ } else {
+ Set commands = new HashSet();
+ for (Map.Entry entry : this.staticCommands.entrySet()) {
+ if (entry.getKey().startsWith(arg) && entry.getValue()
+ .canExecute(player, false)) {
+ commands.add(entry.getValue());
+ }
+ }
+ return commands;
+ }
+ default:
+ Command cmd = getCommand(args[0]);
+ if (cmd != null) {
+ return cmd.tab(player, Arrays.copyOfRange(args, 1, args.length), space);
+ } else {
+ return null;
+ }
+ }
+ }
+
+ @Override public String toString() {
+ return !this.aliases.isEmpty() ? this.aliases.get(0) : this.id;
+ }
+
+ @Override public boolean equals(Object obj) {
+ if (this == obj) {
+ return true;
+ }
+ if (getClass() != obj.getClass()) {
+ return false;
+ }
+ Command other = (Command) obj;
+ if (this.hashCode() != other.hashCode()) {
+ return false;
+ }
+ return this.getFullId().equals(other.getFullId());
+ }
+
+ @Override public int hashCode() {
+ return this.getFullId().hashCode();
+ }
+
+ public void checkTrue(boolean mustBeTrue, C message, Object... args) {
+ if (!mustBeTrue) {
+ throw new CommandException(message, args);
+ }
+ }
+
+ public T check(T object, C message, Object... args) {
+ if (object == null) {
+ throw new CommandException(message, args);
+ }
+ return object;
+ }
+
+ public enum CommandResult {
+ FAILURE, SUCCESS
+ }
+
+
+ public static class CommandException extends RuntimeException {
+ private final Object[] args;
+ private final C message;
+
+ public CommandException(C message, Object... args) {
+ this.message = message;
+ this.args = args;
+ }
+
+ public void perform(PlotPlayer player) {
+ if (player != null && message != null) {
+ message.send(player, args);
+ }
+ }
+ }
+}
diff --git a/Bukkit/src/main/java/com/github/intellectualsites/plotsquared/commands/CommandCaller.java b/Bukkit/src/main/java/com/github/intellectualsites/plotsquared/commands/CommandCaller.java
new file mode 100644
index 000000000..3b5ee295e
--- /dev/null
+++ b/Bukkit/src/main/java/com/github/intellectualsites/plotsquared/commands/CommandCaller.java
@@ -0,0 +1,24 @@
+package com.github.intellectualsites.plotsquared.commands;
+
+import com.github.intellectualsites.plotsquared.plot.commands.RequiredType;
+
+public interface CommandCaller {
+
+ /**
+ * Send the player a message.
+ *
+ * @param message the message to send
+ */
+ void sendMessage(String message);
+
+ /**
+ * Check the player's permissions. Will be cached if permission caching is enabled.
+ *
+ * @param permission the name of the permission
+ */
+ boolean hasPermission(String permission);
+
+ boolean isPermissionSet(String permission);
+
+ RequiredType getSuperCaller();
+}
diff --git a/Bukkit/src/main/java/com/github/intellectualsites/plotsquared/commands/CommandDeclaration.java b/Bukkit/src/main/java/com/github/intellectualsites/plotsquared/commands/CommandDeclaration.java
new file mode 100644
index 000000000..851a03174
--- /dev/null
+++ b/Bukkit/src/main/java/com/github/intellectualsites/plotsquared/commands/CommandDeclaration.java
@@ -0,0 +1,29 @@
+package com.github.intellectualsites.plotsquared.commands;
+
+import com.github.intellectualsites.plotsquared.plot.commands.CommandCategory;
+import com.github.intellectualsites.plotsquared.plot.commands.RequiredType;
+
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+@Target({ElementType.TYPE, ElementType.METHOD}) @Retention(RetentionPolicy.RUNTIME)
+public @interface CommandDeclaration {
+
+ String command();
+
+ String[] aliases() default {};
+
+ String permission() default "";
+
+ String usage() default "";
+
+ String description() default "";
+
+ RequiredType requiredType() default RequiredType.NONE;
+
+ CommandCategory category() default CommandCategory.INFO;
+
+ boolean confirmation() default false;
+}
diff --git a/Bukkit/src/main/java/com/github/intellectualsites/plotsquared/configuration/Configuration.java b/Bukkit/src/main/java/com/github/intellectualsites/plotsquared/configuration/Configuration.java
new file mode 100644
index 000000000..9d694c1ec
--- /dev/null
+++ b/Bukkit/src/main/java/com/github/intellectualsites/plotsquared/configuration/Configuration.java
@@ -0,0 +1,87 @@
+package com.github.intellectualsites.plotsquared.configuration;
+
+import javax.annotation.Nonnull;
+import java.util.Map;
+
+/**
+ * Represents a source of configurable options and settings.
+ */
+public interface Configuration extends ConfigurationSection {
+ /**
+ * Sets the default value of the given path as provided.
+ *
+ *
If no source {@link Configuration} was provided as a default
+ * collection, then a new {@link MemoryConfiguration} will be created to
+ * hold the new default value.
+ *
+ *
If value is null, the value will be removed from the default
+ * Configuration source.
+ *
+ * @param path Path of the value to set.
+ * @param value Value to set the default to.
+ * @throws IllegalArgumentException Thrown if path is null.
+ */
+ @Override void addDefault(@Nonnull String path, Object value);
+
+ /**
+ * Sets the default values of the given paths as provided.
+ *
+ *
If no source {@link Configuration} was provided as a default
+ * collection, then a new {@link MemoryConfiguration} will be created to
+ * hold the new default values.
+ *
+ * @param defaults A map of Path->Values to add to defaults.
+ * @throws IllegalArgumentException Thrown if defaults is null.
+ */
+ void addDefaults(Map defaults);
+
+ /**
+ * Sets the default values of the given paths as provided.
+ *
+ *
If no source {@link Configuration} was provided as a default
+ * collection, then a new {@link MemoryConfiguration} will be created to
+ * hold the new default value.
+ *
+ *
This method will not hold a reference to the specified Configuration,
+ * nor will it automatically update if that Configuration ever changes. If
+ * you check this, you should set the default source with {@link
+ * #setDefaults(Configuration)}.
+ *
+ * @param defaults A configuration holding a list of defaults to copy.
+ * @throws IllegalArgumentException Thrown if defaults is null or this.
+ */
+ void addDefaults(Configuration defaults);
+
+ /**
+ * Gets the source {@link Configuration} for this configuration.
+ *
+ *
+ * If no configuration source was set, but default values were added, then
+ * a {@link MemoryConfiguration} will be returned. If no source was set
+ * and no defaults were set, then this method will return null.
+ *
+ * @return Configuration source for default values, or null if none exist.
+ */
+ Configuration getDefaults();
+
+ /**
+ * Sets the source of all default values for this {@link Configuration}.
+ *
+ *
+ * If a previous source was set, or previous default values were defined,
+ * then they will not be copied to the new source.
+ *
+ * @param defaults New source of default values for this configuration.
+ * @throws IllegalArgumentException Thrown if defaults is null or this.
+ */
+ void setDefaults(Configuration defaults);
+
+ /**
+ * Gets the {@link ConfigurationOptions} for this {@link Configuration}.
+ *
+ *
All setters through this method are chainable.
+ *
+ * @return Options for this configuration
+ */
+ ConfigurationOptions options();
+}
diff --git a/Bukkit/src/main/java/com/github/intellectualsites/plotsquared/configuration/ConfigurationOptions.java b/Bukkit/src/main/java/com/github/intellectualsites/plotsquared/configuration/ConfigurationOptions.java
new file mode 100644
index 000000000..f86016031
--- /dev/null
+++ b/Bukkit/src/main/java/com/github/intellectualsites/plotsquared/configuration/ConfigurationOptions.java
@@ -0,0 +1,90 @@
+package com.github.intellectualsites.plotsquared.configuration;
+
+/**
+ * Various settings for controlling the input and output of a {@link
+ * Configuration}.
+ */
+class ConfigurationOptions {
+ private final Configuration configuration;
+ private char pathSeparator = '.';
+ private boolean copyDefaults = false;
+
+ protected ConfigurationOptions(Configuration configuration) {
+ this.configuration = configuration;
+ }
+
+ /**
+ * Returns the {@link Configuration} that this object is responsible for.
+ *
+ * @return Parent configuration
+ */
+ public Configuration configuration() {
+ return configuration;
+ }
+
+ /**
+ * Gets the char that will be used to separate {@link
+ * ConfigurationSection}s.
+ *
+ * This value does not affect how the {@link Configuration} is stored,
+ * only in how you access the data. The default value is '.'.
+ *
+ * @return Path separator
+ */
+ char pathSeparator() {
+ return pathSeparator;
+ }
+
+ /**
+ * Sets the char that will be used to separate {@link
+ * ConfigurationSection}s.
+ *
+ *
This value does not affect how the {@link Configuration} is stored,
+ * only in how you access the data. The default value is '.'.
+ *
+ * @param value Path separator
+ * @return This object, for chaining
+ */
+ public ConfigurationOptions pathSeparator(char value) {
+ pathSeparator = value;
+ return this;
+ }
+
+ /**
+ * Checks if the {@link Configuration} should copy values from its default
+ * {@link Configuration} directly.
+ *
+ *
If this is true, all values in the default Configuration will be
+ * directly copied, making it impossible to distinguish between values
+ * that were set and values that are provided by default. As a result,
+ * {@link ConfigurationSection#contains(String)} will always
+ * return the same value as {@link
+ * ConfigurationSection#isSet(String)}. The default value is
+ * false.
+ *
+ * @return Whether or not defaults are directly copied
+ */
+ boolean copyDefaults() {
+ return copyDefaults;
+ }
+
+ /**
+ * Sets if the {@link Configuration} should copy values from its default
+ * {@link Configuration} directly.
+ *
+ *
If this is true, all values in the default Configuration will be
+ * directly copied, making it impossible to distinguish between values
+ * that were set and values that are provided by default. As a result,
+ * {@link ConfigurationSection#contains(String)} will always
+ * return the same value as {@link
+ * ConfigurationSection#isSet(String)}. The default value is
+ * false.
+ *
+ * @param value Whether or not defaults are directly copied
+ * @return This object, for chaining
+ */
+ public ConfigurationOptions copyDefaults(boolean value) {
+ copyDefaults = value;
+ return this;
+ }
+}
diff --git a/Bukkit/src/main/java/com/github/intellectualsites/plotsquared/configuration/ConfigurationSection.java b/Bukkit/src/main/java/com/github/intellectualsites/plotsquared/configuration/ConfigurationSection.java
new file mode 100644
index 000000000..9e15a9030
--- /dev/null
+++ b/Bukkit/src/main/java/com/github/intellectualsites/plotsquared/configuration/ConfigurationSection.java
@@ -0,0 +1,649 @@
+package com.github.intellectualsites.plotsquared.configuration;
+
+import javax.annotation.Nonnull;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+/**
+ * Represents a section of a {@link Configuration}.
+ */
+public interface ConfigurationSection {
+
+ /**
+ * Gets a set containing all keys in this section.
+ *
+ *
If deep is set to true, then this will contain all the keys within any
+ * child {@link ConfigurationSection}s (and their children, etc). These
+ * will be in a valid path notation for you to use.
+ *
+ *
If deep is set to false, then this will contain only the keys of any
+ * direct children, and not their own children.
+ *
+ * @param deep Whether or not to get a deep list, as opposed to a shallow
+ * list.
+ * @return Set of keys contained within this ConfigurationSection.
+ */
+ Set getKeys(boolean deep);
+
+ /**
+ * Gets a Map containing all keys and their values for this section.
+ *
+ * If deep is set to true, then this will contain all the keys and values
+ * within any child {@link ConfigurationSection}s (and their children,
+ * etc). These keys will be in a valid path notation for you to use.
+ *
+ *
If deep is set to false, then this will contain only the keys and
+ * values of any direct children, and not their own children.
+ *
+ * @param deep Whether or not to get a deep list, as opposed to a shallow
+ * list.
+ * @return Map of keys and values of this section.
+ */
+ Map getValues(boolean deep);
+
+ /**
+ * Checks if this {@link ConfigurationSection} contains the given path.
+ *
+ * If the value for the requested path does not exist but a default value
+ * has been specified, this will return true.
+ *
+ * @param path Path to check for existence.
+ * @return True if this section contains the requested path, either via
+ * default or being set.
+ * @throws IllegalArgumentException Thrown when path is {@code null}.
+ */
+ boolean contains(@Nonnull String path);
+
+ /**
+ * Checks if this {@link ConfigurationSection} has a value set for the
+ * given path.
+ *
+ *
If the value for the requested path does not exist but a default value
+ * has been specified, this will still return false.
+ *
+ * @param path Path to check for existence.
+ * @return True if this section contains the requested path, regardless of
+ * having a default.
+ * @throws IllegalArgumentException Thrown when path is {@code null}.
+ */
+ boolean isSet(@Nonnull String path);
+
+ /**
+ * Gets the path of this {@link ConfigurationSection} from its root {@link
+ * Configuration}.
+ *
+ *
For any {@link Configuration} themselves, this will return an empty
+ * string.
+ *
+ *
If the section is no longer contained within its root for any reason,
+ * such as being replaced with a different value,
+ * this may return {@code null}.
+ *
+ *
To retrieve the single name of this section, that is, the final part
+ * of the path returned by this method, you may use {@link #getName()}.
+ *
+ * @return Path of this section relative to its root
+ */
+ String getCurrentPath();
+
+ /**
+ * Gets the name of this individual {@link ConfigurationSection}, in the
+ * path.
+ *
+ *
This will always be the final part of {@link #getCurrentPath()}, unless
+ * the section is orphaned.
+ *
+ * @return Name of this section
+ */
+ String getName();
+
+ /**
+ * Gets the root {@link Configuration} that contains this {@link
+ * ConfigurationSection}
+ *
+ *
For any {@link Configuration} themselves, this will return its own
+ * object.
+ *
+ *
If the section is no longer contained within its root for any reason,
+ * such as being replaced with a different value,
+ * this may return {@code null}.
+ *
+ * @return Root configuration containing this section.
+ */
+ Configuration getRoot();
+
+ /**
+ * Gets the parent {@link ConfigurationSection} that directly contains
+ * this {@link ConfigurationSection}.
+ *
+ *
For any {@link Configuration} themselves, this will return
+ * {@code null}.
+ *
+ *
If the section is no longer contained within its parent for any
+ * reason, such as being replaced with a different value, this may
+ * return {@code null}.
+ *
+ * @return Parent section containing this section.
+ */
+ ConfigurationSection getParent();
+
+ /**
+ * Gets the requested Object by path.
+ *
+ *
If the Object does not exist but a default value has been specified,
+ * this will return the default value. If the Object does not exist and no
+ * default value was specified, this will return {@code null}.
+ *
+ * @param path Path of the Object to get.
+ * @return Requested Object.
+ */
+ Object get(String path);
+
+ /**
+ * Gets the requested Object by path, returning a default value if not
+ * found.
+ *
+ *
If the Object does not exist then the specified default value will
+ * returned regardless of if a default has been identified in the root
+ * {@link Configuration}.
+ *
+ * @param path Path of the Object to get.
+ * @param defaultValue The default value to return if the path is not found.
+ * @return Requested Object.
+ */
+ Object getOrDefault(@Nonnull String path, Object defaultValue);
+
+ /**
+ * Sets the specified path to the given value.
+ *
+ *
If value is {@code null}, the entry will be removed. Any
+ * existing entry will be replaced, regardless of what the new value is.
+ *
+ *
Some implementations may have limitations on what you may store. See
+ * their individual javadoc for details. No implementations should allow
+ * you to store {@link Configuration}s or {@link ConfigurationSection}s,
+ * please use {@link #createSection(String)} for that.
+ *
+ * @param path Path of the object to set.
+ * @param value New value to set the path to.
+ */
+ void set(String path, Object value);
+
+ /**
+ * Creates an empty {@link ConfigurationSection} at the specified path.
+ *
+ *
Any value that was previously set at this path will be overwritten. If
+ * the previous value was itself a {@link ConfigurationSection}, it will
+ * be orphaned.
+ *
+ * @param path Path to create the section at.
+ * @return Newly created section
+ */
+ ConfigurationSection createSection(String path);
+
+ /**
+ * Creates a {@link ConfigurationSection} at the specified path, with
+ * specified values.
+ *
+ *
Any value that was previously set at this path will be overwritten. If
+ * the previous value was itself a {@link ConfigurationSection}, it will
+ * be orphaned.
+ *
+ * @param path Path to create the section at.
+ * @param map The values to used.
+ * @return Newly created section
+ */
+ ConfigurationSection createSection(String path, Map, ?> map);
+
+ // Primitives
+
+ /**
+ * Gets the requested String by path.
+ *
+ *
If the String does not exist but a default value has been specified,
+ * this will return the default value. If the String does not exist and no
+ * default value was specified, this will return {@code null}.
+ *
+ * @param path Path of the String to get.
+ * @return Requested String.
+ */
+ String getString(String path);
+
+ /**
+ * Gets the requested String by path, returning a default value if not
+ * found.
+ *
+ *
If the String does not exist then the specified default value will
+ * returned regardless of if a default has been identified in the root
+ * {@link Configuration}.
+ *
+ * @param path Path of the String to get.
+ * @param def The default value to return if the path is not found or is
+ * not a String.
+ * @return Requested String.
+ */
+ String getString(String path, String def);
+
+ /**
+ * Checks if the specified path is a String.
+ *
+ *
If the path exists but is not a String, this will return false. If
+ * the path does not exist, this will return false. If the path does not
+ * exist but a default value has been specified, this will check if that
+ * defaultvalue is a String and return appropriately.
+ *
+ * @param path Path of the String to check.
+ * @return Whether or not the specified path is a String.
+ */
+ boolean isString(String path);
+
+ /**
+ * Gets the requested int by path.
+ *
+ *
If the int does not exist but a default value has been specified, this
+ * will return the default value. If the int does not exist and no default
+ * value was specified, this will return 0.
+ *
+ * @param path Path of the int to get.
+ * @return Requested int.
+ */
+ int getInt(String path);
+
+ /**
+ * Gets the requested int by path, returning a default value if not found.
+ *
+ *
If the int does not exist then the specified default value will
+ * returned regardless of if a default has been identified in the root
+ * {@link Configuration}.
+ *
+ * @param path Path of the int to get.
+ * @param def The default value to return if the path is not found or is
+ * not an int.
+ * @return Requested int.
+ */
+ int getInt(String path, int def);
+
+ /**
+ * Checks if the specified path is an int.
+ *
+ *
If the path exists but is not a int, this will return false. If the
+ * path does not exist, this will return false. If the path does not exist
+ * but a default value has been specified, this will check if that default
+ * value is a int and return appropriately.
+ *
+ * @param path Path of the int to check.
+ * @return Whether or not the specified path is an int.
+ */
+ boolean isInt(String path);
+
+ /**
+ * Gets the requested boolean by path.
+ *
+ *
If the boolean does not exist but a default value has been specified,
+ * this will return the default value. If the boolean does not exist and
+ * no default value was specified, this will return false.
+ *
+ * @param path Path of the boolean to get.
+ * @return Requested boolean.
+ */
+ boolean getBoolean(String path);
+
+ /**
+ * Gets the requested boolean by path, returning a default value if not
+ * found.
+ *
+ *
If the boolean does not exist then the specified default value will
+ * returned regardless of if a default has been identified in the root
+ * {@link Configuration}.
+ *
+ * @param path Path of the boolean to get.
+ * @param defaultValue The default value to return if the path is not found or is
+ * not a boolean.
+ * @return Requested boolean.
+ */
+ boolean getBoolean(String path, boolean defaultValue);
+
+ /**
+ * Checks if the specified path is a boolean.
+ *
+ *
If the path exists but is not a boolean, this will return false. If the
+ * path does not exist, this will return false. If the path does not exist
+ * but a default value has been specified, this will check if that default
+ * value is a boolean and return appropriately.
+ *
+ * @param path Path of the boolean to check.
+ * @return Whether or not the specified path is a boolean.
+ */
+ boolean isBoolean(String path);
+
+ /**
+ * Gets the requested double by path.
+ *
+ *
If the double does not exist but a default value has been specified,
+ * this will return the default value. If the double does not exist and no
+ * default value was specified, this will return 0.
+ *
+ * @param path Path of the double to get.
+ * @return Requested double.
+ */
+ double getDouble(String path);
+
+ /**
+ * Gets the requested double by path, returning a default value if not
+ * found.
+ *
+ *
If the double does not exist then the specified default value will
+ * returned regardless of if a default has been identified in the root
+ * {@link Configuration}.
+ *
+ * @param path Path of the double to get.
+ * @param defaultValue The default value to return if the path is not found or is
+ * not a double.
+ * @return Requested double.
+ */
+ double getDouble(String path, double defaultValue);
+
+ /**
+ * Checks if the specified path is a double.
+ *
+ *
If the path exists but is not a double, this will return false. If the
+ * path does not exist, this will return false. If the path does not exist
+ * but a default value has been specified, this will check if that default
+ * value is a double and return appropriately.
+ *
+ * @param path Path of the double to check.
+ * @return Whether or not the specified path is a double.
+ */
+ boolean isDouble(String path);
+
+ /**
+ * Gets the requested long by path.
+ *
+ *
If the long does not exist but a default value has been specified, this
+ * will return the default value. If the long does not exist and no
+ * default value was specified, this will return 0.
+ *
+ * @param path Path of the long to get.
+ * @return Requested long.
+ */
+ long getLong(String path);
+
+ /**
+ * Gets the requested long by path, returning a default value if not
+ * found.
+ *
+ *
If the long does not exist then the specified default value will
+ * returned regardless of if a default has been identified in the root
+ * {@link Configuration}.
+ *
+ * @param path Path of the long to get.
+ * @param def The default value to return if the path is not found or is
+ * not a long.
+ * @return Requested long.
+ */
+ long getLong(String path, long def);
+
+ /**
+ * Checks if the specified path is a long.
+ *
+ *
If the path exists but is not a long, this will return false. If the
+ * path does not exist, this will return false. If the path does not exist
+ * but a default value has been specified, this will check if that default
+ * value is a long and return appropriately.
+ *
+ * @param path Path of the long to check.
+ * @return Whether or not the specified path is a long.
+ */
+ boolean isLong(String path);
+
+ // Java
+
+ /**
+ * Gets the requested List by path.
+ *
+ *
If the List does not exist but a default value has been specified, this
+ * will return the default value. If the List does not exist and no
+ * default value was specified, this will return null.
+ *
+ * @param path Path of the List to get.
+ * @return Requested List.
+ */
+ List> getList(String path);
+
+ /**
+ * Gets the requested List by path, returning a default value if not
+ * found.
+ *
+ *
If the List does not exist then the specified default value will
+ * returned regardless of if a default has been identified in the root
+ * {@link Configuration}.
+ *
+ * @param path Path of the List to get.
+ * @param def The default value to return if the path is not found or is
+ * not a List.
+ * @return Requested List.
+ */
+ List> getList(String path, List> def);
+
+ /**
+ * Checks if the specified path is a List.
+ *
+ *
If the path exists but is not a List, this will return false. If the
+ * path does not exist, this will return false. If the path does not exist
+ * but a default value has been specified, this will check if that default
+ * value is a List and return appropriately.
+ *
+ * @param path Path of the List to check.
+ * @return Whether or not the specified path is a List.
+ */
+ boolean isList(String path);
+
+ /**
+ * Gets the requested List of String by path.
+ *
+ *
If the List does not exist but a default value has been specified,
+ * this will return the default value. If the List does not exist and no
+ * default value was specified, this will return an empty List.
+ *
+ *
This method will attempt to cast any values into a String if possible,
+ * but may miss any values out if they are not compatible.
+ *
+ * @param path Path of the List to get.
+ * @return Requested List of String.
+ */
+ List getStringList(String path);
+
+ /**
+ * Gets the requested List of Integer by path.
+ *
+ * If the List does not exist but a default value has been specified,
+ * this will return the default value. If the List does not exist and no
+ * default value was specified, this will return an empty List.
+ *
+ *
This method will attempt to cast any values into a Integer if
+ * possible, but may miss any values out if they are not compatible.
+ *
+ * @param path Path of the List to get.
+ * @return Requested List of Integer.
+ */
+ List getIntegerList(String path);
+
+ /**
+ * Gets the requested List of Boolean by path.
+ *
+ * If the List does not exist but a default value has been specified,
+ * this will return the default value. If the List does not exist and no
+ * default value was specified, this will return an empty List.
+ *
+ *
This method will attempt to cast any values into a Boolean if
+ * possible, but may miss any values out if they are not compatible.
+ *
+ * @param path Path of the List to get.
+ * @return Requested List of Boolean.
+ */
+ List getBooleanList(String path);
+
+ /**
+ * Gets the requested List of Double by path.
+ *
+ * If the List does not exist but a default value has been specified,
+ * this will return the default value. If the List does not exist and no
+ * default value was specified, this will return an empty List.
+ *
+ *
This method will attempt to cast any values into a Double if possible,
+ * but may miss any values out if they are not compatible.
+ *
+ * @param path Path of the List to get.
+ * @return Requested List of Double.
+ */
+ List getDoubleList(String path);
+
+ /**
+ * Gets the requested List of Float by path.
+ *
+ * If the List does not exist but a default value has been specified,
+ * this will return the default value. If the List does not exist and no
+ * default value was specified, this will return an empty List.
+ *
+ *
This method will attempt to cast any values into a Float if possible,
+ * but may miss any values out if they are not compatible.
+ *
+ * @param path Path of the List to get.
+ * @return Requested List of Float.
+ */
+ List getFloatList(String path);
+
+ /**
+ * Gets the requested List of Long by path.
+ *
+ * If the List does not exist but a default value has been specified,
+ * this will return the default value. If the List does not exist and no
+ * default value was specified, this will return an empty List.
+ *
+ *
This method will attempt to cast any values into a Long if possible,
+ * but may miss any values out if they are not compatible.
+ *
+ * @param path Path of the List to get.
+ * @return Requested List of Long.
+ */
+ List getLongList(String path);
+
+ /**
+ * Gets the requested List of Byte by path.
+ *
+ * If the List does not exist but a default value has been specified,
+ * this will return the default value. If the List does not exist and no
+ * default value was specified, this will return an empty List.
+ *
+ *
This method will attempt to cast any values into a Byte if possible,
+ * but may miss any values out if they are not compatible.
+ *
+ * @param path Path of the List to get.
+ * @return Requested List of Byte.
+ */
+ List getByteList(String path);
+
+ /**
+ * Gets the requested List of Character by path.
+ *
+ * If the List does not exist but a default value has been specified,
+ * this will return the default value. If the List does not exist and no
+ * default value was specified, this will return an empty List.
+ *
+ *
This method will attempt to cast any values into a Character if
+ * possible, but may miss any values out if they are not compatible.
+ *
+ * @param path Path of the List to get.
+ * @return Requested List of Character.
+ */
+ List getCharacterList(String path);
+
+ /**
+ * Gets the requested List of Short by path.
+ *
+ * If the List does not exist but a default value has been specified,
+ * this will return the default value. If the List does not exist and no
+ * default value was specified, this will return an empty List.
+ *
+ *
This method will attempt to cast any values into a Short if
+ * possible, but may miss any values out if they are not compatible.
+ *
+ * @param path Path of the List to get.
+ * @return Requested List of Short.
+ */
+ List getShortList(String path);
+
+ /**
+ * Gets the requested List of Maps by path.
+ *
+ * If the List does not exist but a default value has been specified,
+ * this will return the default value. If the List does not exist and no
+ * default value was specified, this will return an empty List.
+ *
This method will attempt to cast any values into a Map if possible,
+ * but may miss any values out if they are not compatible.
+ *
+ * @param path Path of the List to get.
+ * @return Requested List of Maps.
+ */
+ List> getMapList(String path);
+
+ /**
+ * Gets the requested ConfigurationSection by path.
+ *
+ * If the ConfigurationSection does not exist but a default value has
+ * been specified, this will return the default value. If the
+ * ConfigurationSection does not exist and no default value was specified,
+ * this will return {@code null}.
+ *
+ * @param path Path of the ConfigurationSection to get.
+ * @return Requested ConfigurationSection.
+ */
+ ConfigurationSection getConfigurationSection(String path);
+
+ /**
+ * Checks if the specified path is a ConfigurationSection.
+ *
+ *
If the path exists but is not a ConfigurationSection, this will return
+ * false. If the path does not exist, this will return false. If the path
+ * does not exist but a default value has been specified, this will check
+ * if that default value is a ConfigurationSection and return
+ * appropriately.
+ *
+ * @param path Path of the ConfigurationSection to check.
+ * @return Whether or not the specified path is a ConfigurationSection.
+ */
+ boolean isConfigurationSection(String path);
+
+ /**
+ * Gets the equivalent {@link ConfigurationSection} from the default
+ * {@link Configuration} defined in {@link #getRoot()}.
+ *
+ *
If the root contains no defaults, or the defaults doesn't contain a
+ * value for this path, or the value at this path is not a {@link
+ * ConfigurationSection} then this will return {@code null}.
+ *
+ * @return Equivalent section in root configuration
+ */
+ ConfigurationSection getDefaultSection();
+
+ /**
+ * Sets the default value in the root at the given path as provided.
+ *
+ *
If no source {@link Configuration} was provided as a default
+ * collection, then a new {@link MemoryConfiguration} will be created to
+ * hold the new default value.
+ *
+ *
If value is {@code null}, the value will be removed from the
+ * default Configuration source.
+ *
+ *
If the value as returned by {@link #getDefaultSection()} is
+ * {@code null}, then this will create a new section at the path,
+ * replacing anything that may have existed there previously.
+ *
+ * @param path Path of the value to set
+ * @param value Value to set the default to
+ * @throws IllegalArgumentException Thrown if path is {@code null}
+ */
+ void addDefault(@Nonnull String path, Object value);
+}
diff --git a/Bukkit/src/main/java/com/github/intellectualsites/plotsquared/configuration/InvalidConfigurationException.java b/Bukkit/src/main/java/com/github/intellectualsites/plotsquared/configuration/InvalidConfigurationException.java
new file mode 100644
index 000000000..e9c099ed1
--- /dev/null
+++ b/Bukkit/src/main/java/com/github/intellectualsites/plotsquared/configuration/InvalidConfigurationException.java
@@ -0,0 +1,45 @@
+package com.github.intellectualsites.plotsquared.configuration;
+
+/**
+ * Exception thrown when attempting to load an invalid {@link Configuration}.
+ */
+@SuppressWarnings("serial") public class InvalidConfigurationException extends Exception {
+
+ /**
+ * Creates a new instance of InvalidConfigurationException without a
+ * message or cause.
+ */
+ public InvalidConfigurationException() {
+ }
+
+ /**
+ * Constructs an instance of InvalidConfigurationException with the
+ * specified message.
+ *
+ * @param msg The details of the exception.
+ */
+ public InvalidConfigurationException(String msg) {
+ super(msg);
+ }
+
+ /**
+ * Constructs an instance of InvalidConfigurationException with the
+ * specified cause.
+ *
+ * @param cause The cause of the exception.
+ */
+ public InvalidConfigurationException(Throwable cause) {
+ super(cause);
+ }
+
+ /**
+ * Constructs an instance of InvalidConfigurationException with the
+ * specified message and cause.
+ *
+ * @param cause The cause of the exception.
+ * @param msg The details of the exception.
+ */
+ public InvalidConfigurationException(String msg, Throwable cause) {
+ super(msg, cause);
+ }
+}
diff --git a/Bukkit/src/main/java/com/github/intellectualsites/plotsquared/configuration/MemoryConfiguration.java b/Bukkit/src/main/java/com/github/intellectualsites/plotsquared/configuration/MemoryConfiguration.java
new file mode 100644
index 000000000..d1677c54d
--- /dev/null
+++ b/Bukkit/src/main/java/com/github/intellectualsites/plotsquared/configuration/MemoryConfiguration.java
@@ -0,0 +1,73 @@
+package com.github.intellectualsites.plotsquared.configuration;
+
+import javax.annotation.Nonnull;
+import java.util.Map;
+
+/**
+ * This is a {@link Configuration} implementation that does not save or load
+ * from any source, and stores all values in memory only.
+ * This is useful for temporary Configurations for providing defaults.
+ */
+public class MemoryConfiguration extends MemorySection implements Configuration {
+ protected Configuration defaults;
+ protected MemoryConfigurationOptions options;
+
+ /**
+ * Creates an empty {@link MemoryConfiguration} with no default values.
+ */
+ public MemoryConfiguration() {
+ }
+
+ /**
+ * Creates an empty {@link MemoryConfiguration} using the specified {@link
+ * Configuration} as a source for all default values.
+ *
+ * @param defaults Default value provider
+ * @throws IllegalArgumentException Thrown if defaults is null
+ */
+ public MemoryConfiguration(Configuration defaults) {
+ this.defaults = defaults;
+ }
+
+ @Override public void addDefault(@Nonnull String path, Object value) {
+ if (this.defaults == null) {
+ this.defaults = new MemoryConfiguration();
+ }
+
+ this.defaults.set(path, value);
+ }
+
+ @Override public void addDefaults(Map defaults) {
+ for (Map.Entry entry : defaults.entrySet()) {
+ addDefault(entry.getKey(), entry.getValue());
+ }
+ }
+
+ @Override public void addDefaults(Configuration defaults) {
+ addDefaults(defaults.getValues(true));
+ }
+
+ @Override public Configuration getDefaults() {
+ return this.defaults;
+ }
+
+ @Override public void setDefaults(Configuration defaults) {
+ if (defaults == null) {
+ throw new NullPointerException("Defaults may not be null");
+ }
+
+ this.defaults = defaults;
+ }
+
+ @Override public ConfigurationSection getParent() {
+ return null;
+ }
+
+ @Override public MemoryConfigurationOptions options() {
+ if (this.options == null) {
+ this.options = new MemoryConfigurationOptions(this);
+ }
+
+ return this.options;
+ }
+}
diff --git a/Bukkit/src/main/java/com/github/intellectualsites/plotsquared/configuration/MemoryConfigurationOptions.java b/Bukkit/src/main/java/com/github/intellectualsites/plotsquared/configuration/MemoryConfigurationOptions.java
new file mode 100644
index 000000000..971ea1e70
--- /dev/null
+++ b/Bukkit/src/main/java/com/github/intellectualsites/plotsquared/configuration/MemoryConfigurationOptions.java
@@ -0,0 +1,25 @@
+package com.github.intellectualsites.plotsquared.configuration;
+
+/**
+ * Various settings for controlling the input and output of a {@link
+ * MemoryConfiguration}.
+ */
+public class MemoryConfigurationOptions extends ConfigurationOptions {
+ protected MemoryConfigurationOptions(MemoryConfiguration configuration) {
+ super(configuration);
+ }
+
+ @Override public MemoryConfiguration configuration() {
+ return (MemoryConfiguration) super.configuration();
+ }
+
+ @Override public MemoryConfigurationOptions copyDefaults(boolean value) {
+ super.copyDefaults(value);
+ return this;
+ }
+
+ @Override public MemoryConfigurationOptions pathSeparator(char value) {
+ super.pathSeparator(value);
+ return this;
+ }
+}
diff --git a/Bukkit/src/main/java/com/github/intellectualsites/plotsquared/configuration/MemorySection.java b/Bukkit/src/main/java/com/github/intellectualsites/plotsquared/configuration/MemorySection.java
new file mode 100644
index 000000000..26f6760c8
--- /dev/null
+++ b/Bukkit/src/main/java/com/github/intellectualsites/plotsquared/configuration/MemorySection.java
@@ -0,0 +1,771 @@
+package com.github.intellectualsites.plotsquared.configuration;
+
+import javax.annotation.Nonnull;
+import java.util.*;
+
+/**
+ * A type of {@link ConfigurationSection} that is stored in memory.
+ */
+public class MemorySection implements ConfigurationSection {
+
+ protected final Map map = new LinkedHashMap<>();
+ private final Configuration root;
+ private final ConfigurationSection parent;
+ private final String path;
+ private final String fullPath;
+
+ /**
+ * Creates an empty MemorySection for use as a root {@link Configuration} section.
+ *
+ * Note that calling this without being yourself a {@link Configuration}
+ * will throw an exception!
+ *
+ * @throws IllegalStateException Thrown if this is not a {@link Configuration} root.
+ */
+ protected MemorySection() {
+ if (!(this instanceof Configuration)) {
+ throw new IllegalStateException(
+ "Cannot construct a root MemorySection when not a Configuration");
+ }
+
+ this.path = "";
+ this.fullPath = "";
+ this.parent = null;
+ this.root = (Configuration) this;
+ }
+
+ /**
+ * Creates an empty MemorySection with the specified parent and path.
+ *
+ * @param parent Parent section that contains this own section.
+ * @param path Path that you may access this section from via the root {@link Configuration}.
+ * @throws IllegalArgumentException Thrown is parent or path is null, or if parent contains no
+ * root Configuration.
+ */
+ protected MemorySection(ConfigurationSection parent, String path) {
+ this.path = path;
+ this.parent = parent;
+ this.root = parent.getRoot();
+
+ if (this.root == null) {
+ throw new NullPointerException("Path may not be orphaned");
+ }
+
+ this.fullPath = createPath(parent, path);
+ }
+
+ public static double toDouble(Object obj, double def) {
+ if (obj instanceof Number) {
+ return ((Number) obj).doubleValue();
+ }
+ if (obj instanceof String) {
+ try {
+ return Double.parseDouble((String) obj);
+ } catch (NumberFormatException ignored) {
+ }
+ } else if (obj instanceof List) {
+ List> val = (List>) obj;
+ if (!val.isEmpty()) {
+ return toDouble(val.get(0), def);
+ }
+ }
+ return def;
+ }
+
+ public static int toInt(Object obj, int def) {
+ if (obj instanceof Number) {
+ return ((Number) obj).intValue();
+ }
+ if (obj instanceof String) {
+ try {
+ return Integer.parseInt((String) obj);
+ } catch (NumberFormatException ignored) {
+ }
+ } else if (obj instanceof List) {
+ List> val = (List>) obj;
+ if (!val.isEmpty()) {
+ return toInt(val.get(0), def);
+ }
+ }
+ return def;
+ }
+
+ public static long toLong(Object obj, long def) {
+ if (obj instanceof Number) {
+ return ((Number) obj).longValue();
+ }
+ if (obj instanceof String) {
+ try {
+ return Long.parseLong((String) obj);
+ } catch (NumberFormatException ignored) {
+ }
+ } else if (obj instanceof List) {
+ List> val = (List>) obj;
+ if (!val.isEmpty()) {
+ return toLong(val.get(0), def);
+ }
+ }
+ return def;
+ }
+
+ /**
+ * Creates a full path to the given {@link ConfigurationSection} from its root {@link
+ * Configuration}.
+ *
+ *
You may use this method for any given {@link ConfigurationSection}, not
+ * only {@link MemorySection}.
+ *
+ * @param section Section to create a path for.
+ * @param key Name of the specified section.
+ * @return Full path of the section from its root.
+ */
+ public static String createPath(ConfigurationSection section, String key) {
+ return createPath(section, key, section.getRoot());
+ }
+
+ /**
+ * Creates a relative path to the given {@link ConfigurationSection} from the given relative
+ * section.
+ *
+ *
You may use this method for any given {@link ConfigurationSection}, not
+ * only {@link MemorySection}.
+ *
+ * @param section Section to create a path for.
+ * @param key Name of the specified section.
+ * @param relativeTo Section to create the path relative to.
+ * @return Full path of the section from its root.
+ */
+ public static String createPath(ConfigurationSection section, String key,
+ ConfigurationSection relativeTo) {
+ Configuration root = section.getRoot();
+ if (root == null) {
+ throw new IllegalStateException("Cannot create path without a root");
+ }
+ char separator = root.options().pathSeparator();
+
+ StringBuilder builder = new StringBuilder();
+ for (ConfigurationSection parent = section;
+ (parent != null) && (parent != relativeTo); parent = parent.getParent()) {
+ if (builder.length() > 0) {
+ builder.insert(0, separator);
+ }
+
+ builder.insert(0, parent.getName());
+ }
+
+ if ((key != null) && !key.isEmpty()) {
+ if (builder.length() > 0) {
+ builder.append(separator);
+ }
+
+ builder.append(key);
+ }
+
+ return builder.toString();
+ }
+
+ @Override public Set getKeys(boolean deep) {
+ Set result = new LinkedHashSet<>();
+
+ Configuration root = getRoot();
+ if ((root != null) && root.options().copyDefaults()) {
+ ConfigurationSection defaults = getDefaultSection();
+
+ if (defaults != null) {
+ result.addAll(defaults.getKeys(deep));
+ }
+ }
+
+ mapChildrenKeys(result, this, deep);
+
+ return result;
+ }
+
+ @Override public Map getValues(boolean deep) {
+ Map result = new LinkedHashMap<>();
+
+ Configuration root = getRoot();
+ if ((root != null) && root.options().copyDefaults()) {
+ ConfigurationSection defaults = getDefaultSection();
+
+ if (defaults != null) {
+ result.putAll(defaults.getValues(deep));
+ }
+ }
+
+ mapChildrenValues(result, this, deep);
+
+ return result;
+ }
+
+ @Override public boolean contains(@Nonnull String path) {
+ return get(path) != null;
+ }
+
+ @Override public boolean isSet(@Nonnull String path) {
+ Configuration root = getRoot();
+ if (root == null) {
+ return false;
+ }
+ if (root.options().copyDefaults()) {
+ return contains(path);
+ }
+ return getOrDefault(path, null) != null;
+ }
+
+ @Override public String getCurrentPath() {
+ return this.fullPath;
+ }
+
+ @Override public String getName() {
+ return this.path;
+ }
+
+ @Override public Configuration getRoot() {
+ return this.root;
+ }
+
+ @Override public ConfigurationSection getParent() {
+ return this.parent;
+ }
+
+ @Override public void addDefault(@Nonnull String path, Object value) {
+ Configuration root = getRoot();
+ if (root == null) {
+ throw new IllegalStateException("Cannot add default without root");
+ }
+ if (root == this) {
+ throw new UnsupportedOperationException(
+ "Unsupported addDefault(String, Object) implementation");
+ }
+ root.addDefault(createPath(this, path), value);
+ }
+
+ @Override public ConfigurationSection getDefaultSection() {
+ Configuration root = getRoot();
+ Configuration defaults = root == null ? null : root.getDefaults();
+
+ if (defaults != null) {
+ if (defaults.isConfigurationSection(getCurrentPath())) {
+ return defaults.getConfigurationSection(getCurrentPath());
+ }
+ }
+
+ return null;
+ }
+
+ @Override public void set(String path, Object value) {
+ Configuration root = getRoot();
+ if (root == null) {
+ throw new IllegalStateException("Cannot use section without a root");
+ }
+
+ char separator = root.options().pathSeparator();
+ // i1 is the leading (higher) index
+ // i2 is the trailing (lower) index
+ int i1 = -1;
+ int i2;
+ ConfigurationSection section = this;
+ while ((i1 = path.indexOf(separator, i2 = i1 + 1)) != -1) {
+ String node = path.substring(i2, i1);
+ ConfigurationSection subSection = section.getConfigurationSection(node);
+ if (subSection == null) {
+ section = section.createSection(node);
+ } else {
+ section = subSection;
+ }
+ }
+
+ String key = path.substring(i2);
+ if (section == this) {
+ if (value == null) {
+ this.map.remove(key);
+ } else {
+ this.map.put(key, value);
+ }
+ } else {
+ section.set(key, value);
+ }
+ }
+
+ @Override public Object get(String path) {
+ return getOrDefault(path, getDefault(path));
+ }
+
+ @Override public Object getOrDefault(@Nonnull String path, Object defaultValue) {
+ if (path.isEmpty()) {
+ return this;
+ }
+
+ Configuration root = getRoot();
+ if (root == null) {
+ throw new IllegalStateException("Cannot access section without a root");
+ }
+
+ char separator = root.options().pathSeparator();
+ // i1 is the leading (higher) index
+ // i2 is the trailing (lower) index
+ int i1 = -1;
+ int i2;
+ ConfigurationSection section = this;
+ while ((i1 = path.indexOf(separator, i2 = i1 + 1)) != -1) {
+ section = section.getConfigurationSection(path.substring(i2, i1));
+ if (section == null) {
+ return defaultValue;
+ }
+ }
+
+ String key = path.substring(i2);
+ if (section == this) {
+ Object result = this.map.get(key);
+ if (result == null) {
+ return defaultValue;
+ } else {
+ return result;
+ }
+ }
+ return section.getOrDefault(key, defaultValue);
+ }
+
+ @Override public ConfigurationSection createSection(String path) {
+ Configuration root = getRoot();
+ if (root == null) {
+ throw new IllegalStateException("Cannot create section without a root");
+ }
+
+ char separator = root.options().pathSeparator();
+ // i1 is the leading (higher) index
+ // i2 is the trailing (lower) index
+ int i1 = -1;
+ int i2;
+ ConfigurationSection section = this;
+ while ((i1 = path.indexOf(separator, i2 = i1 + 1)) != -1) {
+ String node = path.substring(i2, i1);
+ ConfigurationSection subSection = section.getConfigurationSection(node);
+ if (subSection == null) {
+ section = section.createSection(node);
+ } else {
+ section = subSection;
+ }
+ }
+
+ String key = path.substring(i2);
+ if (section == this) {
+ ConfigurationSection result = new MemorySection(this, key);
+ this.map.put(key, result);
+ return result;
+ }
+ return section.createSection(key);
+ }
+
+ @Override public ConfigurationSection createSection(String path, Map, ?> map) {
+ ConfigurationSection section = createSection(path);
+
+ for (Map.Entry, ?> entry : map.entrySet()) {
+ if (entry.getValue() instanceof Map) {
+ section.createSection(entry.getKey().toString(), (Map, ?>) entry.getValue());
+ } else {
+ section.set(entry.getKey().toString(), entry.getValue());
+ }
+ }
+
+ return section;
+ }
+
+ // Primitives
+ @Override public String getString(String path) {
+ Object def = getDefault(path);
+ return getString(path, def != null ? def.toString() : null);
+ }
+
+ @Override public String getString(String path, String def) {
+ Object val = getOrDefault(path, def);
+ if (val != null) {
+ return val.toString();
+ } else {
+ return def;
+ }
+ }
+
+ @Override public boolean isString(String path) {
+ Object val = get(path);
+ return val instanceof String;
+ }
+
+ @Override public int getInt(String path) {
+ Object def = getDefault(path);
+ return getInt(path, toInt(def, 0));
+ }
+
+ @Override public int getInt(String path, int def) {
+ Object val = getOrDefault(path, def);
+ return toInt(val, def);
+ }
+
+ @Override public boolean isInt(String path) {
+ Object val = get(path);
+ return val instanceof Integer;
+ }
+
+ @Override public boolean getBoolean(String path) {
+ Object def = getDefault(path);
+ if (def instanceof Boolean) {
+ return getBoolean(path, (Boolean) def);
+ } else {
+ return getBoolean(path, false);
+ }
+ }
+
+ @Override public boolean getBoolean(String path, boolean defaultValue) {
+ Object val = getOrDefault(path, defaultValue);
+ if (val instanceof Boolean) {
+ return (Boolean) val;
+ } else {
+ return defaultValue;
+ }
+ }
+
+ @Override public boolean isBoolean(String path) {
+ Object val = get(path);
+ return val instanceof Boolean;
+ }
+
+ @Override public double getDouble(String path) {
+ Object def = getDefault(path);
+ return getDouble(path, toDouble(def, 0));
+ }
+
+ @Override public double getDouble(String path, double defaultValue) {
+ Object val = getOrDefault(path, defaultValue);
+ return toDouble(val, defaultValue);
+ }
+
+ @Override public boolean isDouble(String path) {
+ Object val = get(path);
+ return val instanceof Double;
+ }
+
+ @Override public long getLong(String path) {
+ Object def = getDefault(path);
+ return getLong(path, toLong(def, 0));
+ }
+
+ @Override public long getLong(String path, long def) {
+ Object val = getOrDefault(path, def);
+ return toLong(val, def);
+ }
+
+ @Override public boolean isLong(String path) {
+ Object val = get(path);
+ return val instanceof Long;
+ }
+
+ // Java
+ @Override public List> getList(String path) {
+ Object def = getDefault(path);
+ return getList(path, def instanceof List ? (List>) def : null);
+ }
+
+ @Override public List> getList(String path, List> def) {
+ Object val = getOrDefault(path, def);
+ return (List>) ((val instanceof List) ? val : def);
+ }
+
+ @Override public boolean isList(String path) {
+ Object val = get(path);
+ return val instanceof List;
+ }
+
+ @Override public List getStringList(String path) {
+ List> list = getList(path);
+
+ if (list == null) {
+ return new ArrayList<>(0);
+ }
+
+ List result = new ArrayList<>();
+
+ for (Object object : list) {
+ if ((object instanceof String) || isPrimitiveWrapper(object)) {
+ result.add(String.valueOf(object));
+ }
+ }
+
+ return result;
+ }
+
+ @Override public List getIntegerList(String path) {
+ List> list = getList(path);
+
+ List result = new ArrayList<>();
+
+ for (Object object : list) {
+ if (object instanceof Integer) {
+ result.add((Integer) object);
+ } else if (object instanceof String) {
+ try {
+ result.add(Integer.valueOf((String) object));
+ } catch (NumberFormatException ignored) {
+ }
+ } else if (object instanceof Character) {
+ result.add((int) (Character) object);
+ } else if (object instanceof Number) {
+ result.add(((Number) object).intValue());
+ }
+ }
+
+ return result;
+ }
+
+ @Override public List getBooleanList(String path) {
+ List> list = getList(path);
+
+ List result = new ArrayList<>();
+
+ for (Object object : list) {
+ if (object instanceof Boolean) {
+ result.add((Boolean) object);
+ } else if (object instanceof String) {
+ if (Boolean.TRUE.toString().equals(object)) {
+ result.add(true);
+ } else if (Boolean.FALSE.toString().equals(object)) {
+ result.add(false);
+ }
+ }
+ }
+
+ return result;
+ }
+
+ @Override public List getDoubleList(String path) {
+ List> list = getList(path);
+
+ List result = new ArrayList<>();
+
+ for (Object object : list) {
+ if (object instanceof Double) {
+ result.add((Double) object);
+ } else if (object instanceof String) {
+ try {
+ result.add(Double.valueOf((String) object));
+ } catch (NumberFormatException ignored) {
+ }
+ } else if (object instanceof Character) {
+ result.add((double) (Character) object);
+ } else if (object instanceof Number) {
+ result.add(((Number) object).doubleValue());
+ }
+ }
+
+ return result;
+ }
+
+ @Override public List getFloatList(String path) {
+ List> list = getList(path);
+
+ List result = new ArrayList<>();
+
+ for (Object object : list) {
+ if (object instanceof Float) {
+ result.add((Float) object);
+ } else if (object instanceof String) {
+ try {
+ result.add(Float.valueOf((String) object));
+ } catch (NumberFormatException ignored) {
+ }
+ } else if (object instanceof Character) {
+ result.add((float) (Character) object);
+ } else if (object instanceof Number) {
+ result.add(((Number) object).floatValue());
+ }
+ }
+
+ return result;
+ }
+
+ @Override public List getLongList(String path) {
+ List> list = getList(path);
+
+ List result = new ArrayList<>();
+
+ for (Object object : list) {
+ if (object instanceof Long) {
+ result.add((Long) object);
+ } else if (object instanceof String) {
+ try {
+ result.add(Long.valueOf((String) object));
+ } catch (NumberFormatException ignored) {
+ }
+ } else if (object instanceof Character) {
+ result.add((long) (Character) object);
+ } else if (object instanceof Number) {
+ result.add(((Number) object).longValue());
+ }
+ }
+
+ return result;
+ }
+
+ @Override public List getByteList(String path) {
+ List> list = getList(path);
+
+ List result = new ArrayList<>();
+
+ for (Object object : list) {
+ if (object instanceof Byte) {
+ result.add((Byte) object);
+ } else if (object instanceof String) {
+ try {
+ result.add(Byte.valueOf((String) object));
+ } catch (NumberFormatException ignored) {
+ }
+ } else if (object instanceof Character) {
+ result.add((byte) ((Character) object).charValue());
+ } else if (object instanceof Number) {
+ result.add(((Number) object).byteValue());
+ }
+ }
+
+ return result;
+ }
+
+ @Override public List getCharacterList(String path) {
+ List> list = getList(path);
+
+ List result = new ArrayList<>();
+
+ for (Object object : list) {
+ if (object instanceof Character) {
+ result.add((Character) object);
+ } else if (object instanceof String) {
+ String str = (String) object;
+
+ if (str.length() == 1) {
+ result.add(str.charAt(0));
+ }
+ } else if (object instanceof Number) {
+ result.add((char) ((Number) object).intValue());
+ }
+ }
+
+ return result;
+ }
+
+ @Override public List getShortList(String path) {
+ List> list = getList(path);
+
+ List result = new ArrayList<>();
+
+ for (Object object : list) {
+ if (object instanceof Short) {
+ result.add((Short) object);
+ } else if (object instanceof String) {
+ try {
+ result.add(Short.valueOf((String) object));
+ } catch (NumberFormatException ignored) {
+ }
+ } else if (object instanceof Character) {
+ result.add((short) ((Character) object).charValue());
+ } else if (object instanceof Number) {
+ result.add(((Number) object).shortValue());
+ }
+ }
+
+ return result;
+ }
+
+ @Override public List> getMapList(String path) {
+ List> list = getList(path);
+ List> result = new ArrayList<>();
+
+ for (Object object : list) {
+ if (object instanceof Map) {
+ result.add((Map, ?>) object);
+ }
+ }
+
+ return result;
+ }
+
+ @Override public ConfigurationSection getConfigurationSection(String path) {
+ Object val = getOrDefault(path, null);
+ if (val != null) {
+ return (val instanceof ConfigurationSection) ? (ConfigurationSection) val : null;
+ }
+
+ val = getOrDefault(path, getDefault(path));
+ return (val instanceof ConfigurationSection) ? createSection(path) : null;
+ }
+
+ @Override public boolean isConfigurationSection(String path) {
+ Object val = get(path);
+ return val instanceof ConfigurationSection;
+ }
+
+ protected boolean isPrimitiveWrapper(Object input) {
+ return (input instanceof Integer) || (input instanceof Boolean)
+ || (input instanceof Character) || (input instanceof Byte) || (input instanceof Short)
+ || (input instanceof Double) || (input instanceof Long) || (input instanceof Float);
+ }
+
+ protected Object getDefault(String path) {
+ Configuration root = getRoot();
+ Configuration defaults = root == null ? null : root.getDefaults();
+ return (defaults == null) ? null : defaults.get(createPath(this, path));
+ }
+
+ protected void mapChildrenKeys(Set output, ConfigurationSection section, boolean deep) {
+ if (section instanceof MemorySection) {
+ MemorySection sec = (MemorySection) section;
+
+ for (Map.Entry entry : sec.map.entrySet()) {
+ output.add(createPath(section, entry.getKey(), this));
+
+ if (deep && (entry.getValue() instanceof ConfigurationSection)) {
+ ConfigurationSection subsection = (ConfigurationSection) entry.getValue();
+ mapChildrenKeys(output, subsection, deep);
+ }
+ }
+ } else {
+ Set keys = section.getKeys(deep);
+
+ for (String key : keys) {
+ output.add(createPath(section, key, this));
+ }
+ }
+ }
+
+ protected void mapChildrenValues(Map output, ConfigurationSection section,
+ boolean deep) {
+ if (section instanceof MemorySection) {
+ MemorySection sec = (MemorySection) section;
+
+ for (Map.Entry entry : sec.map.entrySet()) {
+ output.put(createPath(section, entry.getKey(), this), entry.getValue());
+
+ if (entry.getValue() instanceof ConfigurationSection) {
+ if (deep) {
+ mapChildrenValues(output, (ConfigurationSection) entry.getValue(), deep);
+ }
+ }
+ }
+ } else {
+ Map values = section.getValues(deep);
+
+ for (Map.Entry entry : values.entrySet()) {
+ output.put(createPath(section, entry.getKey(), this), entry.getValue());
+ }
+ }
+ }
+
+ @Override public String toString() {
+ Configuration root = getRoot();
+ if (root == null) {
+ return getClass().getSimpleName() + "[path='" + getCurrentPath() + "', root='" + null
+ + "']";
+ } else {
+ return getClass().getSimpleName() + "[path='" + getCurrentPath() + "', root='" + root
+ .getClass().getSimpleName() + "']";
+ }
+ }
+}
diff --git a/Bukkit/src/main/java/com/github/intellectualsites/plotsquared/configuration/file/FileConfiguration.java b/Bukkit/src/main/java/com/github/intellectualsites/plotsquared/configuration/file/FileConfiguration.java
new file mode 100644
index 000000000..bc0b375e9
--- /dev/null
+++ b/Bukkit/src/main/java/com/github/intellectualsites/plotsquared/configuration/file/FileConfiguration.java
@@ -0,0 +1,158 @@
+package com.github.intellectualsites.plotsquared.configuration.file;
+
+import com.github.intellectualsites.plotsquared.configuration.Configuration;
+import com.github.intellectualsites.plotsquared.configuration.InvalidConfigurationException;
+import com.github.intellectualsites.plotsquared.configuration.MemoryConfiguration;
+
+import javax.annotation.Nonnull;
+import java.io.*;
+import java.nio.charset.StandardCharsets;
+
+/**
+ * This is a base class for all File based implementations of {@link
+ * Configuration}.
+ */
+public abstract class FileConfiguration extends MemoryConfiguration {
+
+ /**
+ * Creates an empty {@link FileConfiguration} with no default values.
+ */
+ FileConfiguration() {
+ }
+
+ /**
+ * Creates an empty {@link FileConfiguration} using the specified {@link
+ * Configuration} as a source for all default values.
+ *
+ * @param defaults Default value provider
+ */
+ public FileConfiguration(Configuration defaults) {
+ super(defaults);
+ }
+
+ /**
+ * Saves this {@link FileConfiguration} to the specified location.
+ *
+ * If the file does not exist, it will be created. If already exists, it
+ * will be overwritten. If it cannot be overwritten or created, an
+ * exception will be thrown.
+ *
+ *
This method will save using the system default encoding, or possibly
+ * using UTF8.
+ *
+ * @param file File to save to.
+ * @throws IOException Thrown when the given file cannot be written to for
+ * any reason.
+ */
+ public void save(File file) throws IOException {
+ File parent = file.getParentFile();
+ if (parent != null) {
+ parent.mkdirs();
+ }
+
+ String data = saveToString();
+
+ try (Writer writer = new OutputStreamWriter(new FileOutputStream(file),
+ StandardCharsets.UTF_8)) {
+ writer.write(data);
+ }
+ }
+
+ /**
+ * Saves this {@link FileConfiguration} to a string, and returns it.
+ *
+ * @return String containing this configuration.
+ */
+ public abstract String saveToString();
+
+ /**
+ * Loads this {@link FileConfiguration} from the specified location.
+ *
+ *
All the values contained within this configuration will be removed,
+ * leaving only settings and defaults, and the new values will be loaded
+ * from the given file.
+ *
+ *
If the file cannot be loaded for any reason, an exception will be
+ * thrown.
+ *
+ * @param file File to load from.
+ * @throws FileNotFoundException Thrown when the given file cannot be
+ * opened.
+ * @throws IOException Thrown when the given file cannot be read.
+ * @throws InvalidConfigurationException Thrown when the given file is not
+ * a valid Configuration.
+ * @throws IllegalArgumentException Thrown when file is null.
+ */
+ public void load(@Nonnull File file) throws IOException, InvalidConfigurationException {
+
+ FileInputStream stream = new FileInputStream(file);
+
+ load(new InputStreamReader(stream, StandardCharsets.UTF_8));
+ }
+
+ /**
+ * Loads this {@link FileConfiguration} from the specified reader.
+ *
+ *
All the values contained within this configuration will be removed,
+ * leaving only settings and defaults, and the new values will be loaded
+ * from the given stream.
+ *
+ * @param reader the reader to load from
+ * @throws IOException thrown when underlying reader throws an IOException
+ * @throws InvalidConfigurationException thrown when the reader does not
+ * represent a valid Configuration
+ */
+ public void load(Reader reader) throws IOException, InvalidConfigurationException {
+
+ StringBuilder builder = new StringBuilder();
+
+ try (BufferedReader input = reader instanceof BufferedReader ?
+ (BufferedReader) reader :
+ new BufferedReader(reader)) {
+ String line;
+
+ while ((line = input.readLine()) != null) {
+ builder.append(line);
+ builder.append('\n');
+ }
+ }
+
+ loadFromString(builder.toString());
+ }
+
+ /**
+ * Loads this {@link FileConfiguration} from the specified string, as
+ * opposed to from file.
+ *
+ *
All the values contained within this configuration will be removed,
+ * leaving only settings and defaults, and the new values will be loaded
+ * from the given string.
+ *
+ *
If the string is invalid in any way, an exception will be thrown.
+ *
+ * @param contents Contents of a Configuration to load.
+ * @throws InvalidConfigurationException Thrown if the specified string is
+ * invalid.
+ */
+ public abstract void loadFromString(String contents) throws InvalidConfigurationException;
+
+ /**
+ * Compiles the header for this {@link FileConfiguration} and returns the
+ * result.
+ *
+ *
This will use the header from {@link #options()} -> {@link
+ * FileConfigurationOptions#header()}, respecting the rules of {@link
+ * FileConfigurationOptions#copyHeader()} if set.
+ *
+ * @return Compiled header
+ */
+ protected abstract String buildHeader();
+
+ @Override public FileConfigurationOptions options() {
+ if (this.options == null) {
+ this.options = new FileConfigurationOptions(this);
+ }
+
+ return (FileConfigurationOptions) this.options;
+ }
+}
diff --git a/Bukkit/src/main/java/com/github/intellectualsites/plotsquared/configuration/file/FileConfigurationOptions.java b/Bukkit/src/main/java/com/github/intellectualsites/plotsquared/configuration/file/FileConfigurationOptions.java
new file mode 100644
index 000000000..a216d0566
--- /dev/null
+++ b/Bukkit/src/main/java/com/github/intellectualsites/plotsquared/configuration/file/FileConfigurationOptions.java
@@ -0,0 +1,115 @@
+package com.github.intellectualsites.plotsquared.configuration.file;
+
+import com.github.intellectualsites.plotsquared.configuration.Configuration;
+import com.github.intellectualsites.plotsquared.configuration.MemoryConfiguration;
+import com.github.intellectualsites.plotsquared.configuration.MemoryConfigurationOptions;
+
+/**
+ * Various settings for controlling the input and output of a {@link
+ * FileConfiguration}.
+ */
+public class FileConfigurationOptions extends MemoryConfigurationOptions {
+ private String header = null;
+ private boolean copyHeader = true;
+
+ protected FileConfigurationOptions(MemoryConfiguration configuration) {
+ super(configuration);
+ }
+
+ @Override public FileConfiguration configuration() {
+ return (FileConfiguration) super.configuration();
+ }
+
+ @Override public FileConfigurationOptions copyDefaults(boolean value) {
+ super.copyDefaults(value);
+ return this;
+ }
+
+ @Override public FileConfigurationOptions pathSeparator(char value) {
+ super.pathSeparator(value);
+ return this;
+ }
+
+ /**
+ * Gets the header that will be applied to the top of the saved output.
+ *
+ *
This header will be commented out and applied directly at the top of
+ * the generated output of the {@link FileConfiguration}. It is not
+ * required to include a newline at the end of the header as it will
+ * automatically be applied, but you may include one if you wish for extra
+ * spacing.
+ *
+ *
{@code null} is a valid value which will indicate that no header]
+ * is to be applied. The default value is {@code null}.
+ *
+ * @return Header
+ */
+ public String header() {
+ return header;
+ }
+
+ /**
+ * Sets the header that will be applied to the top of the saved output.
+ *
+ *
This header will be commented out and applied directly at the top of
+ * the generated output of the {@link FileConfiguration}. It is not
+ * required to include a newline at the end of the header as it will
+ * automatically be applied, but you may include one if you wish for extra
+ * spacing.
+ *
+ *
{@code null} is a valid value which will indicate that no header
+ * is to be applied.
+ *
+ * @param value New header
+ * @return This object, for chaining
+ */
+ public FileConfigurationOptions header(String value) {
+ header = value;
+ return this;
+ }
+
+ /**
+ * Gets whether or not the header should be copied from a default source.
+ *
+ *
If this is true, if a default {@link FileConfiguration} is passed to
+ * {@link FileConfiguration#setDefaults(Configuration)}
+ * then upon saving it will use the header from that config, instead of
+ * the one provided here.
+ *
+ *
If no default is set on the configuration, or the default is not of
+ * type FileConfiguration, or that config has no header ({@link #header()}
+ * returns null) then the header specified in this configuration will be
+ * used.
+ *
+ *
Defaults to true.
+ *
+ * @return Whether or not to copy the header
+ */
+ public boolean copyHeader() {
+ return copyHeader;
+ }
+
+ /**
+ * Sets whether or not the header should be copied from a default source.
+ *
+ *
If this is true, if a default {@link FileConfiguration} is passed to
+ * {@link FileConfiguration#setDefaults(Configuration)}
+ * then upon saving it will use the header from that config, instead of
+ * the one provided here.
+ *
+ *
If no default is set on the configuration, or the default is not of
+ * type FileConfiguration, or that config has no header ({@link #header()}
+ * returns null) then the header specified in this configuration will be
+ * used.
+ *
+ *
Defaults to true.
+ *
+ * @param value Whether or not to copy the header
+ * @return This object, for chaining
+ */
+ public FileConfigurationOptions copyHeader(boolean value) {
+ copyHeader = value;
+
+ return this;
+ }
+}
diff --git a/Bukkit/src/main/java/com/github/intellectualsites/plotsquared/configuration/file/YamlConfiguration.java b/Bukkit/src/main/java/com/github/intellectualsites/plotsquared/configuration/file/YamlConfiguration.java
new file mode 100644
index 000000000..1e694f05a
--- /dev/null
+++ b/Bukkit/src/main/java/com/github/intellectualsites/plotsquared/configuration/file/YamlConfiguration.java
@@ -0,0 +1,189 @@
+package com.github.intellectualsites.plotsquared.configuration.file;
+
+import com.github.intellectualsites.plotsquared.configuration.Configuration;
+import com.github.intellectualsites.plotsquared.configuration.ConfigurationSection;
+import com.github.intellectualsites.plotsquared.configuration.InvalidConfigurationException;
+import com.github.intellectualsites.plotsquared.plot.PlotSquared;
+import org.yaml.snakeyaml.DumperOptions;
+import org.yaml.snakeyaml.Yaml;
+import org.yaml.snakeyaml.error.YAMLException;
+import org.yaml.snakeyaml.representer.Representer;
+
+import java.io.File;
+import java.io.IOException;
+import java.nio.file.Files;
+import java.nio.file.StandardCopyOption;
+import java.util.Map;
+
+/**
+ * An implementation of {@link Configuration} which saves all files in Yaml.
+ * Note that this implementation is not synchronized.
+ */
+public class YamlConfiguration extends FileConfiguration {
+ private static final String COMMENT_PREFIX = "# ";
+ private static final String BLANK_CONFIG = "{}\n";
+ private final DumperOptions yamlOptions = new DumperOptions();
+ private final Representer yamlRepresenter = new YamlRepresenter();
+ private final Yaml yaml = new Yaml(new YamlConstructor(), yamlRepresenter, yamlOptions);
+
+ /**
+ * Creates a new {@link YamlConfiguration}, loading from the given file.
+ *
+ *
Any errors loading the Configuration will be logged and then ignored.
+ * If the specified input is not a valid config, a blank config will be
+ * returned.
+ *
+ *
The encoding used may follow the system dependent default.
+ *
+ * @param file Input file
+ * @return Resulting configuration
+ */
+ public static YamlConfiguration loadConfiguration(File file) {
+ YamlConfiguration config = new YamlConfiguration();
+
+ try {
+ config.load(file);
+ } catch (InvalidConfigurationException | IOException ex) {
+ try {
+ File dest = new File(file.getAbsolutePath() + "_broken");
+ int i = 0;
+ while (dest.exists()) {
+ dest = new File(file.getAbsolutePath() + "_broken_" + i++);
+ }
+ Files.copy(file.toPath(), dest.toPath(), StandardCopyOption.REPLACE_EXISTING);
+ PlotSquared.debug("&dCould not read: &7" + file);
+ PlotSquared.debug("&dRenamed to: &7" + dest.getName());
+ PlotSquared.debug("&c============ Full stacktrace ============");
+ ex.printStackTrace();
+ PlotSquared.debug("&c=========================================");
+ } catch (IOException e) {
+ e.printStackTrace();
+ }
+ }
+
+ return config;
+ }
+
+ @Override public String saveToString() {
+ yamlOptions.setIndent(options().indent());
+ yamlOptions.setDefaultFlowStyle(DumperOptions.FlowStyle.BLOCK);
+ yamlRepresenter.setDefaultFlowStyle(DumperOptions.FlowStyle.BLOCK);
+
+ String header = buildHeader();
+ String dump = yaml.dump(getValues(false));
+
+ if (dump.equals(BLANK_CONFIG)) {
+ dump = "";
+ }
+
+ return header + dump;
+ }
+
+ @Override public void loadFromString(String contents) throws InvalidConfigurationException {
+
+ Map, ?> input;
+ try {
+ input = yaml.load(contents);
+ } catch (YAMLException e) {
+ throw new InvalidConfigurationException(e);
+ } catch (ClassCastException ignored) {
+ throw new InvalidConfigurationException("Top level is not a Map.");
+ }
+
+ String header = parseHeader(contents);
+ if (!header.isEmpty()) {
+ options().header(header);
+ }
+
+ if (input != null) {
+ convertMapsToSections(input, this);
+ }
+ }
+
+ protected void convertMapsToSections(Map, ?> input, ConfigurationSection section) {
+ for (Map.Entry, ?> entry : input.entrySet()) {
+ String key = entry.getKey().toString();
+ Object value = entry.getValue();
+
+ if (value instanceof Map) {
+ convertMapsToSections((Map, ?>) value, section.createSection(key));
+ } else {
+ section.set(key, value);
+ }
+ }
+ }
+
+ protected String parseHeader(String input) {
+ String[] lines = input.split("\r?\n", -1);
+ StringBuilder result = new StringBuilder();
+ boolean readingHeader = true;
+ boolean foundHeader = false;
+
+ for (int i = 0; (i < lines.length) && readingHeader; i++) {
+ String line = lines[i];
+
+ if (line.startsWith(COMMENT_PREFIX)) {
+ if (i > 0) {
+ result.append('\n');
+ }
+
+ if (line.length() > COMMENT_PREFIX.length()) {
+ result.append(line.substring(COMMENT_PREFIX.length()));
+ }
+
+ foundHeader = true;
+ } else if (foundHeader && line.isEmpty()) {
+ result.append('\n');
+ } else if (foundHeader) {
+ readingHeader = false;
+ }
+ }
+
+ return result.toString();
+ }
+
+ @Override protected String buildHeader() {
+ String header = options().header();
+
+ if (options().copyHeader()) {
+ Configuration def = getDefaults();
+
+ if (def instanceof FileConfiguration) {
+ FileConfiguration fileDefaults = (FileConfiguration) def;
+ String defaultsHeader = fileDefaults.buildHeader();
+
+ if ((defaultsHeader != null) && !defaultsHeader.isEmpty()) {
+ return defaultsHeader;
+ }
+ }
+ }
+
+ if (header == null) {
+ return "";
+ }
+
+ StringBuilder builder = new StringBuilder();
+ String[] lines = header.split("\r?\n", -1);
+ boolean startedHeader = false;
+
+ for (int i = lines.length - 1; i >= 0; i--) {
+ builder.insert(0, '\n');
+
+ if (startedHeader || !lines[i].isEmpty()) {
+ builder.insert(0, lines[i]);
+ builder.insert(0, COMMENT_PREFIX);
+ startedHeader = true;
+ }
+ }
+
+ return builder.toString();
+ }
+
+ @Override public YamlConfigurationOptions options() {
+ if (options == null) {
+ options = new YamlConfigurationOptions(this);
+ }
+
+ return (YamlConfigurationOptions) options;
+ }
+}
diff --git a/Bukkit/src/main/java/com/github/intellectualsites/plotsquared/configuration/file/YamlConfigurationOptions.java b/Bukkit/src/main/java/com/github/intellectualsites/plotsquared/configuration/file/YamlConfigurationOptions.java
new file mode 100644
index 000000000..730527652
--- /dev/null
+++ b/Bukkit/src/main/java/com/github/intellectualsites/plotsquared/configuration/file/YamlConfigurationOptions.java
@@ -0,0 +1,68 @@
+package com.github.intellectualsites.plotsquared.configuration.file;
+
+/**
+ * Various settings for controlling the input and output of a {@link
+ * YamlConfiguration}.
+ */
+public class YamlConfigurationOptions extends FileConfigurationOptions {
+ private int indent = 2;
+
+ YamlConfigurationOptions(YamlConfiguration configuration) {
+ super(configuration);
+ }
+
+ @Override public YamlConfiguration configuration() {
+ return (YamlConfiguration) super.configuration();
+ }
+
+ @Override public YamlConfigurationOptions copyDefaults(boolean value) {
+ super.copyDefaults(value);
+ return this;
+ }
+
+ @Override public YamlConfigurationOptions pathSeparator(char value) {
+ super.pathSeparator(value);
+ return this;
+ }
+
+ @Override public YamlConfigurationOptions header(String value) {
+ super.header(value);
+ return this;
+ }
+
+ @Override public YamlConfigurationOptions copyHeader(boolean value) {
+ super.copyHeader(value);
+ return this;
+ }
+
+ /**
+ * Gets how much spaces should be used to indent each line.
+ *
+ *
The minimum value this may be is 2, and the maximum is 9.
+ *
+ * @return How much to indent by
+ */
+ int indent() {
+ return indent;
+ }
+
+ /**
+ * Sets how much spaces should be used to indent each line.
+ *
+ *
The minimum value this may be is 2, and the maximum is 9.
+ *
+ * @param value New indent
+ * @return This object, for chaining
+ */
+ public YamlConfigurationOptions indent(int value) {
+ if (value < 2) {
+ throw new IllegalArgumentException("Indent must be at least 2 characters");
+ }
+ if (value > 9) {
+ throw new IllegalArgumentException("Indent cannot be greater than 9 characters");
+ }
+
+ indent = value;
+ return this;
+ }
+}
diff --git a/Bukkit/src/main/java/com/github/intellectualsites/plotsquared/configuration/file/YamlConstructor.java b/Bukkit/src/main/java/com/github/intellectualsites/plotsquared/configuration/file/YamlConstructor.java
new file mode 100644
index 000000000..cdc5eb821
--- /dev/null
+++ b/Bukkit/src/main/java/com/github/intellectualsites/plotsquared/configuration/file/YamlConstructor.java
@@ -0,0 +1,46 @@
+package com.github.intellectualsites.plotsquared.configuration.file;
+
+import com.github.intellectualsites.plotsquared.configuration.serialization.ConfigurationSerialization;
+import org.yaml.snakeyaml.constructor.SafeConstructor;
+import org.yaml.snakeyaml.error.YAMLException;
+import org.yaml.snakeyaml.nodes.Node;
+import org.yaml.snakeyaml.nodes.Tag;
+
+import java.util.LinkedHashMap;
+import java.util.Map;
+
+class YamlConstructor extends SafeConstructor {
+
+ YamlConstructor() {
+ yamlConstructors.put(Tag.MAP, new ConstructCustomObject());
+ }
+
+ private class ConstructCustomObject extends ConstructYamlMap {
+ @Override public Object construct(final Node node) {
+ if (node.isTwoStepsConstruction()) {
+ throw new YAMLException("Unexpected referential mapping structure. Node: " + node);
+ }
+
+ final Map, ?> raw = (Map, ?>) super.construct(node);
+
+ if (raw.containsKey(ConfigurationSerialization.SERIALIZED_TYPE_KEY)) {
+ final Map typed = new LinkedHashMap<>(raw.size());
+ for (final Map.Entry, ?> entry : raw.entrySet()) {
+ typed.put(entry.getKey().toString(), entry.getValue());
+ }
+
+ try {
+ return ConfigurationSerialization.deserializeObject(typed);
+ } catch (final IllegalArgumentException ex) {
+ throw new YAMLException("Could not deserialize object", ex);
+ }
+ }
+
+ return raw;
+ }
+
+ @Override public void construct2ndStep(final Node node, final Object object) {
+ throw new YAMLException("Unexpected referential mapping structure. Node: " + node);
+ }
+ }
+}
diff --git a/Bukkit/src/main/java/com/github/intellectualsites/plotsquared/configuration/file/YamlRepresenter.java b/Bukkit/src/main/java/com/github/intellectualsites/plotsquared/configuration/file/YamlRepresenter.java
new file mode 100644
index 000000000..ebe1aebee
--- /dev/null
+++ b/Bukkit/src/main/java/com/github/intellectualsites/plotsquared/configuration/file/YamlRepresenter.java
@@ -0,0 +1,40 @@
+package com.github.intellectualsites.plotsquared.configuration.file;
+
+import com.github.intellectualsites.plotsquared.configuration.ConfigurationSection;
+import com.github.intellectualsites.plotsquared.configuration.serialization.ConfigurationSerializable;
+import com.github.intellectualsites.plotsquared.configuration.serialization.ConfigurationSerialization;
+import org.yaml.snakeyaml.nodes.Node;
+import org.yaml.snakeyaml.representer.Representer;
+
+import java.util.LinkedHashMap;
+import java.util.Map;
+
+class YamlRepresenter extends Representer {
+
+ YamlRepresenter() {
+ this.multiRepresenters.put(ConfigurationSection.class, new RepresentConfigurationSection());
+ this.multiRepresenters
+ .put(ConfigurationSerializable.class, new RepresentConfigurationSerializable());
+ }
+
+ private class RepresentConfigurationSection extends RepresentMap {
+
+ @Override public Node representData(Object data) {
+ return super.representData(((ConfigurationSection) data).getValues(false));
+ }
+ }
+
+
+ private class RepresentConfigurationSerializable extends RepresentMap {
+
+ @Override public Node representData(Object data) {
+ ConfigurationSerializable serializable = (ConfigurationSerializable) data;
+ Map values = new LinkedHashMap<>();
+ values.put(ConfigurationSerialization.SERIALIZED_TYPE_KEY,
+ ConfigurationSerialization.getAlias(serializable.getClass()));
+ values.putAll(serializable.serialize());
+
+ return super.representData(values);
+ }
+ }
+}
diff --git a/Bukkit/src/main/java/com/github/intellectualsites/plotsquared/configuration/serialization/ConfigurationSerializable.java b/Bukkit/src/main/java/com/github/intellectualsites/plotsquared/configuration/serialization/ConfigurationSerializable.java
new file mode 100644
index 000000000..f83e7a468
--- /dev/null
+++ b/Bukkit/src/main/java/com/github/intellectualsites/plotsquared/configuration/serialization/ConfigurationSerializable.java
@@ -0,0 +1,34 @@
+package com.github.intellectualsites.plotsquared.configuration.serialization;
+
+import java.util.Map;
+
+/**
+ * Represents an object that may be serialized.
+ * These objects MUST implement one of the following, in addition to
+ * the methods as defined by this interface:
+ *
+ * A static method "deserialize" that accepts a single {@link Map}<
+ * {@link String}, {@link Object}> and returns the class.
+ * A static method "valueOf" that accepts a single {@link Map}<{@link
+ * String}, {@link Object}> and returns the class.
+ * A constructor that accepts a single {@link Map}<{@link String},
+ * {@link Object}>.
+ *
+ * In addition to implementing this interface, you must register the class
+ * with {@link ConfigurationSerialization#registerClass(Class)}.
+ *
+ * @see DelegateDeserialization
+ * @see SerializableAs
+ */
+public interface ConfigurationSerializable {
+
+ /**
+ * Creates a Map representation of this class.
+ *
+ * This class must provide a method to restore this class, as defined in
+ * the {@link ConfigurationSerializable} interface javadoc.
+ *
+ * @return Map containing the current state of this class
+ */
+ Map serialize();
+}
diff --git a/Bukkit/src/main/java/com/github/intellectualsites/plotsquared/configuration/serialization/ConfigurationSerialization.java b/Bukkit/src/main/java/com/github/intellectualsites/plotsquared/configuration/serialization/ConfigurationSerialization.java
new file mode 100644
index 000000000..0783b4af5
--- /dev/null
+++ b/Bukkit/src/main/java/com/github/intellectualsites/plotsquared/configuration/serialization/ConfigurationSerialization.java
@@ -0,0 +1,262 @@
+package com.github.intellectualsites.plotsquared.configuration.serialization;
+
+import com.github.intellectualsites.plotsquared.configuration.Configuration;
+
+import java.lang.reflect.Constructor;
+import java.lang.reflect.InvocationTargetException;
+import java.lang.reflect.Method;
+import java.lang.reflect.Modifier;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.logging.Level;
+import java.util.logging.Logger;
+
+/**
+ * Utility class for storing and retrieving classes for {@link Configuration}.
+ */
+public class ConfigurationSerialization {
+
+ public static final String SERIALIZED_TYPE_KEY = "==";
+ private static final Map> aliases =
+ new HashMap<>();
+ private final Class extends ConfigurationSerializable> clazz;
+
+ protected ConfigurationSerialization(Class extends ConfigurationSerializable> clazz) {
+ this.clazz = clazz;
+ }
+
+ /**
+ * Attempts to deserialize the given arguments into a new instance of the
+ * given class.
+ *
+ *
The class must implement {@link ConfigurationSerializable}, including
+ * the extra methods as specified in the javadoc of
+ * ConfigurationSerializable.
+ *
+ *
If a new instance could not be made, an example being the class not
+ * fully implementing the interface, null will be returned.
+ *
+ * @param args Arguments for deserialization
+ * @param clazz Class to deserialize into
+ * @return New instance of the specified class
+ */
+ public static ConfigurationSerializable deserializeObject(Map args,
+ Class extends ConfigurationSerializable> clazz) {
+ return new ConfigurationSerialization(clazz).deserialize(args);
+ }
+
+ /**
+ * Attempts to deserialize the given arguments into a new instance of the
+ *
+ * given class.
+ *
+ * The class must implement {@link ConfigurationSerializable}, including
+ * the extra methods as specified in the javadoc of
+ * ConfigurationSerializable.
+ *
+ *
+ * If a new instance could not be made, an example being the class not
+ * fully implementing the interface, null will be returned.
+ *
+ * @param args Arguments for deserialization
+ * @return New instance of the specified class
+ */
+ public static ConfigurationSerializable deserializeObject(Map args) {
+ Class extends ConfigurationSerializable> clazz = null;
+
+ if (args.containsKey(SERIALIZED_TYPE_KEY)) {
+ try {
+ String alias = (String) args.get(SERIALIZED_TYPE_KEY);
+
+ if (alias == null) {
+ throw new IllegalArgumentException("Cannot have null alias");
+ }
+ clazz = getClassByAlias(alias);
+ if (clazz == null) {
+ throw new IllegalArgumentException(
+ "Specified class does not exist ('" + alias + "')");
+ }
+ } catch (ClassCastException ex) {
+ ex.fillInStackTrace();
+ throw ex;
+ }
+ } else {
+ throw new IllegalArgumentException(
+ "Args doesn't contain type key ('" + SERIALIZED_TYPE_KEY + "')");
+ }
+
+ return new ConfigurationSerialization(clazz).deserialize(args);
+ }
+
+ /**
+ * Registers the given {@link ConfigurationSerializable} class by its
+ * alias.
+ *
+ * @param clazz Class to register
+ */
+ public static void registerClass(Class extends ConfigurationSerializable> clazz) {
+ DelegateDeserialization delegate = clazz.getAnnotation(DelegateDeserialization.class);
+
+ if (delegate == null) {
+ registerClass(clazz, getAlias(clazz));
+ registerClass(clazz, clazz.getName());
+ }
+ }
+
+ /**
+ * Registers the given alias to the specified {@link
+ * ConfigurationSerializable} class.
+ *
+ * @param clazz Class to register
+ * @param alias Alias to register as
+ * @see SerializableAs
+ */
+ public static void registerClass(Class extends ConfigurationSerializable> clazz,
+ String alias) {
+ aliases.put(alias, clazz);
+ }
+
+ /**
+ * Unregisters the specified alias to a {@link ConfigurationSerializable}
+ *
+ * @param alias Alias to unregister
+ */
+ public static void unregisterClass(String alias) {
+ aliases.remove(alias);
+ }
+
+ /**
+ * Unregisters any aliases for the specified {@link
+ * ConfigurationSerializable} class.
+ *
+ * @param clazz Class to unregister
+ */
+ public static void unregisterClass(Class extends ConfigurationSerializable> clazz) {
+ while (aliases.values().remove(clazz)) {
+ }
+ }
+
+ /**
+ * Attempts to get a registered {@link ConfigurationSerializable} class by
+ * its alias.
+ *
+ * @param alias Alias of the serializable
+ * @return Registered class, or null if not found
+ */
+ public static Class extends ConfigurationSerializable> getClassByAlias(String alias) {
+ return aliases.get(alias);
+ }
+
+ /**
+ * Gets the correct alias for the given {@link ConfigurationSerializable}
+ * class.
+ *
+ * @param clazz Class to get alias for
+ * @return Alias to use for the class
+ */
+ public static String getAlias(Class extends ConfigurationSerializable> clazz) {
+ DelegateDeserialization delegate = clazz.getAnnotation(DelegateDeserialization.class);
+
+ if (delegate != null) {
+ if (delegate.value() == clazz) {
+ delegate = null;
+ } else {
+ return getAlias(delegate.value());
+ }
+ }
+
+ SerializableAs alias = clazz.getAnnotation(SerializableAs.class);
+
+ if (alias != null) {
+ return alias.value();
+ }
+
+ return clazz.getName();
+ }
+
+ protected Method getMethod(String name, boolean isStatic) {
+ try {
+ Method method = this.clazz.getDeclaredMethod(name, Map.class);
+
+ if (!ConfigurationSerializable.class.isAssignableFrom(method.getReturnType())) {
+ return null;
+ }
+ if (Modifier.isStatic(method.getModifiers()) != isStatic) {
+ return null;
+ }
+
+ return method;
+ } catch (NoSuchMethodException | SecurityException ignored) {
+ return null;
+ }
+ }
+
+ protected Constructor extends ConfigurationSerializable> getConstructor() {
+ try {
+ return this.clazz.getConstructor(Map.class);
+ } catch (NoSuchMethodException | SecurityException ignored) {
+ return null;
+ }
+ }
+
+ protected ConfigurationSerializable deserializeViaMethod(Method method, Map args) {
+ try {
+ ConfigurationSerializable result =
+ (ConfigurationSerializable) method.invoke(null, args);
+
+ if (result == null) {
+ Logger.getLogger(ConfigurationSerialization.class.getName()).log(Level.SEVERE,
+ "Could not call method '" + method.toString() + "' of " + this.clazz
+ + " for deserialization: method returned null");
+ } else {
+ return result;
+ }
+ } catch (IllegalAccessException | InvocationTargetException | IllegalArgumentException ex) {
+ Logger.getLogger(ConfigurationSerialization.class.getName()).log(Level.SEVERE,
+ "Could not call method '" + method.toString() + "' of " + this.clazz
+ + " for deserialization",
+ ex instanceof InvocationTargetException ? ex.getCause() : ex);
+ }
+
+ return null;
+ }
+
+ protected ConfigurationSerializable deserializeViaCtor(
+ Constructor extends ConfigurationSerializable> ctor, Map args) {
+ try {
+ return ctor.newInstance(args);
+ } catch (IllegalAccessException | IllegalArgumentException | InvocationTargetException | InstantiationException ex) {
+ Logger.getLogger(ConfigurationSerialization.class.getName()).log(Level.SEVERE,
+ "Could not call constructor '" + ctor.toString() + "' of " + this.clazz
+ + " for deserialization",
+ ex instanceof InvocationTargetException ? ex.getCause() : ex);
+ }
+
+ return null;
+ }
+
+ public ConfigurationSerializable deserialize(Map args) {
+ if (args == null) {
+ throw new NullPointerException("Args must not be null");
+ }
+ ConfigurationSerializable result = null;
+ Method method = getMethod("deserialize", true);
+ if (method != null) {
+ result = deserializeViaMethod(method, args);
+ }
+ if (result == null) {
+ method = getMethod("valueOf", true);
+ if (method != null) {
+ result = deserializeViaMethod(method, args);
+ }
+ }
+ if (result == null) {
+ Constructor extends ConfigurationSerializable> constructor = getConstructor();
+ if (constructor != null) {
+ result = deserializeViaCtor(constructor, args);
+ }
+ }
+
+ return result;
+ }
+}
diff --git a/Bukkit/src/main/java/com/github/intellectualsites/plotsquared/configuration/serialization/DelegateDeserialization.java b/Bukkit/src/main/java/com/github/intellectualsites/plotsquared/configuration/serialization/DelegateDeserialization.java
new file mode 100644
index 000000000..8a071cb2e
--- /dev/null
+++ b/Bukkit/src/main/java/com/github/intellectualsites/plotsquared/configuration/serialization/DelegateDeserialization.java
@@ -0,0 +1,21 @@
+package com.github.intellectualsites.plotsquared.configuration.serialization;
+
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+/**
+ * Applies to a {@link ConfigurationSerializable} that will delegate all
+ * deserialization to another {@link ConfigurationSerializable}.
+ */
+@Retention(RetentionPolicy.RUNTIME) @Target(ElementType.TYPE)
+public @interface DelegateDeserialization {
+ /**
+ * Which class should be used as a delegate for this classes
+ * deserialization
+ *
+ * @return Delegate class
+ */
+ Class extends ConfigurationSerializable> value();
+}
diff --git a/Bukkit/src/main/java/com/github/intellectualsites/plotsquared/configuration/serialization/SerializableAs.java b/Bukkit/src/main/java/com/github/intellectualsites/plotsquared/configuration/serialization/SerializableAs.java
new file mode 100644
index 000000000..79b50998c
--- /dev/null
+++ b/Bukkit/src/main/java/com/github/intellectualsites/plotsquared/configuration/serialization/SerializableAs.java
@@ -0,0 +1,32 @@
+package com.github.intellectualsites.plotsquared.configuration.serialization;
+
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+/**
+ * Represents an "alias" that a {@link ConfigurationSerializable} may be
+ * stored as.
+ * If this is not present on a {@link ConfigurationSerializable} class, it
+ * will use the fully qualified name of the class.
+ *
+ * This value will be stored in the configuration so that the configuration
+ * deserialization can determine what type it is.
+ *
+ * Using this annotation on any other class than a {@link
+ * ConfigurationSerializable} will have no effect.
+ *
+ * @see ConfigurationSerialization#registerClass(Class, String)
+ */
+@Retention(RetentionPolicy.RUNTIME) @Target(ElementType.TYPE) public @interface SerializableAs {
+ /**
+ * This is the name your class will be stored and retrieved as.
+ *
+ * This name MUST be unique. We recommend using names such as
+ * "MyPluginThing" instead of "Thing".
+ *
+ * @return Name to serialize the class as.
+ */
+ String value();
+}
diff --git a/Bukkit/src/main/java/com/github/intellectualsites/plotsquared/json/JSONArray.java b/Bukkit/src/main/java/com/github/intellectualsites/plotsquared/json/JSONArray.java
new file mode 100644
index 000000000..62ee01329
--- /dev/null
+++ b/Bukkit/src/main/java/com/github/intellectualsites/plotsquared/json/JSONArray.java
@@ -0,0 +1,815 @@
+package com.github.intellectualsites.plotsquared.json;
+
+import java.io.IOException;
+import java.io.StringWriter;
+import java.io.Writer;
+import java.lang.reflect.Array;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Map;
+
+/**
+ * A JSONArray is an ordered sequence of values. Its external text form is a string wrapped in square brackets with
+ * commas separating the values. The internal form is an object having {@code get} and {@code opt} methods for
+ * accessing the values by index, and {@code put} methods for adding or replacing values. The values can be any of
+ * these types: {@code Boolean}, {@code JSONArray}, {@code JSONObject}, {@code Number},
+ * {@code String}, or the {@code JSONObject.NULL object}.
+ *
+ *
The constructor can convert a JSON text into a Java object. The {@code toString} method converts to JSON text.
+ *
+ *
A {@code get} method returns a value if one can be found, and throws an exception if one cannot be found. An
+ * {@code opt} method returns a default value instead of throwing an exception, and so is useful for obtaining
+ * optional values.
+ *
+ *
The generic {@code get()} and {@code opt()} methods return an object which you can cast or query for type.
+ * There are also typed {@code get} and {@code opt} methods that do type checking and type coercion for you.
+ *
+ *
The texts produced by the {@code toString} methods strictly conform to JSON syntax rules. The constructors are
+ * more forgiving in the texts they will accept:
An extra {@code ,} (comma) may appear
+ * just before the closing bracket. The {@code null} value will be inserted when there is {@code ,}
+ * (comma) elision. Strings may be quoted with {@code '} (single
+ * quote) . Strings do not need to be quoted at all if they do not begin with a quote or single quote,
+ * and if they do not contain leading or trailing spaces, and if they do not contain any of these characters: {@code { }
+ * [ ] / \ : , #} and if they do not look like numbers and if they are not the reserved words {@code true},
+ * {@code false}, or {@code null}.
+ *
+ * @author JSON.org
+ * @version 2014-05-03
+ */
+public class JSONArray {
+ /**
+ * The arrayList where the JSONArray's properties are kept.
+ */
+ private final ArrayList myArrayList;
+
+ /**
+ * Construct an empty JSONArray.
+ */
+ public JSONArray() {
+ this.myArrayList = new ArrayList();
+ }
+
+ /**
+ * Construct a JSONArray from a JSONTokener.
+ *
+ * @param x A JSONTokener
+ * @throws JSONException If there is a syntax error.
+ */
+ public JSONArray(JSONTokener x) throws JSONException {
+ this();
+ if (x.nextClean() != '[') {
+ throw x.syntaxError("A JSONArray text must start with '['");
+ }
+ if (x.nextClean() != ']') {
+ x.back();
+ for (; ; ) {
+ if (x.nextClean() == ',') {
+ x.back();
+ this.myArrayList.add(JSONObject.NULL);
+ } else {
+ x.back();
+ this.myArrayList.add(x.nextValue());
+ }
+ switch (x.nextClean()) {
+ case ',':
+ if (x.nextClean() == ']') {
+ return;
+ }
+ x.back();
+ break;
+ case ']':
+ return;
+ default:
+ throw x.syntaxError("Expected a ',' or ']'");
+ }
+ }
+ }
+ }
+
+ /**
+ * Construct a JSONArray from a source JSON text.
+ *
+ * @param source A string that begins with {@code [} (left bracket) and ends with
+ * {@code ]} (right bracket) .
+ * @throws JSONException If there is a syntax error.
+ */
+ public JSONArray(String source) throws JSONException {
+ this(new JSONTokener(source));
+ }
+
+ /**
+ * Construct a JSONArray from a Collection.
+ *
+ * @param collection A Collection.
+ */
+ public JSONArray(Collection collection) {
+ this.myArrayList = new ArrayList();
+ if (collection != null) {
+ for (Object aCollection : collection) {
+ this.myArrayList.add(JSONObject.wrap(aCollection));
+ }
+ }
+ }
+
+ /**
+ * Construct a JSONArray from an array
+ *
+ * @throws JSONException If not an array.
+ */
+ public JSONArray(Object array) throws JSONException {
+ this();
+ if (array.getClass().isArray()) {
+ int length = Array.getLength(array);
+ for (int i = 0; i < length; i += 1) {
+ this.put(JSONObject.wrap(Array.get(array, i)));
+ }
+ } else {
+ throw new JSONException(
+ "JSONArray initial value should be a string or collection or array.");
+ }
+ }
+
+ /**
+ * Get the object value associated with an index.
+ *
+ * @param index The index must be between 0 and length() - 1.
+ * @return An object value.
+ * @throws JSONException If there is no value for the index.
+ */
+ public Object get(int index) throws JSONException {
+ Object object = opt(index);
+ if (object == null) {
+ throw new JSONException("JSONArray[" + index + "] not found.");
+ }
+ return object;
+ }
+
+ /**
+ * Get the boolean value associated with an index. The string values "true" and "false" are converted to boolean.
+ *
+ * @param index The index must be between 0 and length() - 1.
+ * @return The truth.
+ * @throws JSONException If there is no value for the index or if the value is not convertible to boolean.
+ */
+ public boolean getBoolean(int index) throws JSONException {
+ Object object = get(index);
+ if (object.equals(Boolean.FALSE) || ((object instanceof String) && ((String) object)
+ .equalsIgnoreCase("false"))) {
+ return false;
+ } else if (object.equals(Boolean.TRUE) || ((object instanceof String) && ((String) object)
+ .equalsIgnoreCase("true"))) {
+ return true;
+ }
+ throw new JSONException("JSONArray[" + index + "] is not a boolean.");
+ }
+
+ /**
+ * Get the double value associated with an index.
+ *
+ * @param index The index must be between 0 and length() - 1.
+ * @return The value.
+ * @throws JSONException If the key is not found or if the value cannot be converted to a number.
+ */
+ public double getDouble(int index) throws JSONException {
+ Object object = get(index);
+ try {
+ return object instanceof Number ?
+ ((Number) object).doubleValue() :
+ Double.parseDouble((String) object);
+ } catch (NumberFormatException ignored) {
+ throw new JSONException("JSONArray[" + index + "] is not a number.");
+ }
+ }
+
+ /**
+ * Get the int value associated with an index.
+ *
+ * @param index The index must be between 0 and length() - 1.
+ * @return The value.
+ * @throws JSONException If the key is not found or if the value is not a number.
+ */
+ public int getInt(int index) throws JSONException {
+ Object object = get(index);
+ try {
+ return object instanceof Number ?
+ ((Number) object).intValue() :
+ Integer.parseInt((String) object);
+ } catch (NumberFormatException ignored) {
+ throw new JSONException("JSONArray[" + index + "] is not a number.");
+ }
+ }
+
+ /**
+ * Get the JSONArray associated with an index.
+ *
+ * @param index The index must be between 0 and length() - 1.
+ * @return A JSONArray value.
+ * @throws JSONException If there is no value for the index. or if the value is not a JSONArray
+ */
+ public JSONArray getJSONArray(int index) throws JSONException {
+ Object object = get(index);
+ if (object instanceof JSONArray) {
+ return (JSONArray) object;
+ }
+ throw new JSONException("JSONArray[" + index + "] is not a JSONArray.");
+ }
+
+ /**
+ * Get the JSONObject associated with an index.
+ *
+ * @param index subscript
+ * @return A JSONObject value.
+ * @throws JSONException If there is no value for the index or if the value is not a JSONObject
+ */
+ public JSONObject getJSONObject(int index) throws JSONException {
+ Object object = get(index);
+ if (object instanceof JSONObject) {
+ return (JSONObject) object;
+ }
+ throw new JSONException("JSONArray[" + index + "] is not a JSONObject.");
+ }
+
+ /**
+ * Get the long value associated with an index.
+ *
+ * @param index The index must be between 0 and length() - 1.
+ * @return The value.
+ * @throws JSONException If the key is not found or if the value cannot be converted to a number.
+ */
+ public long getLong(int index) throws JSONException {
+ Object object = get(index);
+ try {
+ return object instanceof Number ?
+ ((Number) object).longValue() :
+ Long.parseLong((String) object);
+ } catch (NumberFormatException ignored) {
+ throw new JSONException("JSONArray[" + index + "] is not a number.");
+ }
+ }
+
+ /**
+ * Get the string associated with an index.
+ *
+ * @param index The index must be between 0 and length() - 1.
+ * @return A string value.
+ * @throws JSONException If there is no string value for the index.
+ */
+ public String getString(int index) throws JSONException {
+ Object object = get(index);
+ if (object instanceof String) {
+ return (String) object;
+ }
+ throw new JSONException("JSONArray[" + index + "] not a string.");
+ }
+
+ /**
+ * Determine if the value is null.
+ *
+ * @param index The index must be between 0 and length() - 1.
+ * @return true if the value at the index is null, or if there is no value.
+ */
+ public boolean isNull(int index) {
+ return JSONObject.NULL.equals(opt(index));
+ }
+
+ /**
+ * Make a string from the contents of this JSONArray. The {@code separator} string is inserted between each
+ * element. Warning: This method assumes that the data structure is acyclical.
+ *
+ * @param separator A string that will be inserted between the elements.
+ * @return a string.
+ * @throws JSONException If the array contains an invalid number.
+ */
+ public String join(String separator) throws JSONException {
+ int len = length();
+ StringBuilder sb = new StringBuilder();
+ for (int i = 0; i < len; i += 1) {
+ if (i > 0) {
+ sb.append(separator);
+ }
+ sb.append(JSONObject.valueToString(this.myArrayList.get(i)));
+ }
+ return sb.toString();
+ }
+
+ /**
+ * Get the number of elements in the JSONArray, included nulls.
+ *
+ * @return The length (or size).
+ */
+ public int length() {
+ return this.myArrayList.size();
+ }
+
+ /**
+ * Get the optional object value associated with an index.
+ *
+ * @param index The index must be between 0 and length() - 1.
+ * @return An object value, or null if there is no object at that index.
+ */
+ public Object opt(int index) {
+ return ((index < 0) || (index >= length())) ? null : this.myArrayList.get(index);
+ }
+
+ /**
+ * Get the optional boolean value associated with an index. It returns false if there is no value at that index, or
+ * if the value is not Boolean.TRUE or the String "true".
+ *
+ * @param index The index must be between 0 and length() - 1.
+ * @return The truth.
+ */
+ public boolean optBoolean(int index) {
+ return this.optBoolean(index, false);
+ }
+
+ /**
+ * Get the optional boolean value associated with an index. It returns the defaultValue if there is no value at that
+ * index or if it is not a Boolean or the String "true" or "false" (case insensitive).
+ *
+ * @param index The index must be between 0 and length() - 1.
+ * @param defaultValue A boolean default.
+ * @return The truth.
+ */
+ public boolean optBoolean(int index, boolean defaultValue) {
+ try {
+ return getBoolean(index);
+ } catch (JSONException ignored) {
+ return defaultValue;
+ }
+ }
+
+ /**
+ * Get the optional double value associated with an index. NaN is returned if there is no value for the index, or if
+ * the value is not a number and cannot be converted to a number.
+ *
+ * @param index The index must be between 0 and length() - 1.
+ * @return The value.
+ */
+ public double optDouble(int index) {
+ return this.optDouble(index, Double.NaN);
+ }
+
+ /**
+ * Get the optional double value associated with an index. The defaultValue is returned if there is no value for the
+ * index, or if the value is not a number and cannot be converted to a number.
+ *
+ * @param index subscript
+ * @param defaultValue The default value.
+ * @return The value.
+ */
+ public double optDouble(int index, double defaultValue) {
+ try {
+ return getDouble(index);
+ } catch (JSONException ignored) {
+ return defaultValue;
+ }
+ }
+
+ /**
+ * Get the optional int value associated with an index. Zero is returned if there is no value for the index, or if
+ * the value is not a number and cannot be converted to a number.
+ *
+ * @param index The index must be between 0 and length() - 1.
+ * @return The value.
+ */
+ public int optInt(int index) {
+ return this.optInt(index, 0);
+ }
+
+ /**
+ * Get the optional int value associated with an index. The defaultValue is returned if there is no value for the
+ * index, or if the value is not a number and cannot be converted to a number.
+ *
+ * @param index The index must be between 0 and length() - 1.
+ * @param defaultValue The default value.
+ * @return The value.
+ */
+ public int optInt(int index, int defaultValue) {
+ try {
+ return getInt(index);
+ } catch (JSONException ignored) {
+ return defaultValue;
+ }
+ }
+
+ /**
+ * Get the optional JSONArray associated with an index.
+ *
+ * @param index subscript
+ * @return A JSONArray value, or null if the index has no value, or if the value is not a JSONArray.
+ */
+ public JSONArray optJSONArray(int index) {
+ Object o = opt(index);
+ return o instanceof JSONArray ? (JSONArray) o : null;
+ }
+
+ /**
+ * Get the optional JSONObject associated with an index. Null is returned if the key is not found, or null if the
+ * index has no value, or if the value is not a JSONObject.
+ *
+ * @param index The index must be between 0 and length() - 1.
+ * @return A JSONObject value.
+ */
+ public JSONObject optJSONObject(int index) {
+ Object o = opt(index);
+ return o instanceof JSONObject ? (JSONObject) o : null;
+ }
+
+ /**
+ * Get the optional long value associated with an index. Zero is returned if there is no value for the index, or if
+ * the value is not a number and cannot be converted to a number.
+ *
+ * @param index The index must be between 0 and length() - 1.
+ * @return The value.
+ */
+ public long optLong(int index) {
+ return this.optLong(index, 0);
+ }
+
+ /**
+ * Get the optional long value associated with an index. The defaultValue
+ * is returned if there is no value for the index, or if the value is not a
+ * number and cannot be converted to a number.
+ *
+ * @param index The index must be between 0 and length() - 1.
+ * @param defaultValue The default value.
+ * @return The value.
+ */
+ public long optLong(int index, long defaultValue) {
+ try {
+ return getLong(index);
+ } catch (JSONException ignored) {
+ return defaultValue;
+ }
+ }
+
+ /**
+ * Get the optional string value associated with an index. It returns an
+ * empty string if there is no value at that index. If the value is not a
+ * string and is not null, then it is converted to a string.
+ *
+ * @param index The index must be between 0 and length() - 1.
+ * @return A String value.
+ */
+ public String optString(int index) {
+ return this.optString(index, "");
+ }
+
+ /**
+ * Get the optional string associated with an index. The defaultValue is
+ * returned if the key is not found.
+ *
+ * @param index The index must be between 0 and length() - 1.
+ * @param defaultValue The default value.
+ * @return A String value.
+ */
+ public String optString(int index, String defaultValue) {
+ Object object = opt(index);
+ return JSONObject.NULL.equals(object) ? defaultValue : object.toString();
+ }
+
+ /**
+ * Append a boolean value. This increases the array's length by one.
+ *
+ * @param value A boolean value.
+ * @return this.
+ */
+ public JSONArray put(boolean value) {
+ this.put(value ? Boolean.TRUE : Boolean.FALSE);
+ return this;
+ }
+
+ /**
+ * Put a value in the JSONArray, where the value will be a JSONArray which
+ * is produced from a Collection.
+ *
+ * @param value A Collection value.
+ * @return this.
+ */
+ public JSONArray put(Collection value) {
+ this.put(new JSONArray(value));
+ return this;
+ }
+
+ /**
+ * Append a double value. This increases the array's length by one.
+ *
+ * @param value A double value.
+ * @return this.
+ * @throws JSONException if the value is not finite.
+ */
+ public JSONArray put(double value) throws JSONException {
+ Double d = value;
+ JSONObject.testValidity(d);
+ this.put(d);
+ return this;
+ }
+
+ /**
+ * Append an int value. This increases the array's length by one.
+ *
+ * @param value An int value.
+ * @return this.
+ */
+ public JSONArray put(int value) {
+ this.put(Integer.valueOf(value));
+ return this;
+ }
+
+ /**
+ * Append an long value. This increases the array's length by one.
+ *
+ * @param value A long value.
+ * @return this.
+ */
+ public JSONArray put(long value) {
+ this.put(Long.valueOf(value));
+ return this;
+ }
+
+ /**
+ * Put a value in the JSONArray, where the value will be a JSONObject which
+ * is produced from a Map.
+ *
+ * @param value A Map value.
+ * @return this.
+ */
+ public JSONArray put(Map value) {
+ this.put(new JSONObject(value));
+ return this;
+ }
+
+ /**
+ * Append an object value. This increases the array's length by one.
+ *
+ * @param value An object value. The value should be a Boolean, Double,
+ * Integer, JSONArray, JSONObject, Long, or String, or the JSONObject.NULL object.
+ * @return this.
+ */
+ public JSONArray put(Object value) {
+ this.myArrayList.add(value);
+ return this;
+ }
+
+ /**
+ * Put or replace a boolean value in the JSONArray. If the index is greater than the length of the JSONArray, then
+ * null elements will be added as necessary to pad it out.
+ *
+ * @param index The subscript.
+ * @param value A boolean value.
+ * @return this.
+ * @throws JSONException If the index is negative.
+ */
+ public JSONArray put(int index, boolean value) throws JSONException {
+ this.put(index, value ? Boolean.TRUE : Boolean.FALSE);
+ return this;
+ }
+
+ /**
+ * Put a value in the JSONArray, where the value will be a JSONArray which
+ * is produced from a Collection.
+ *
+ * @param index The subscript.
+ * @param value A Collection value.
+ * @return this.
+ * @throws JSONException If the index is negative or if the value is not
+ * finite.
+ */
+ public JSONArray put(int index, Collection value) throws JSONException {
+ this.put(index, new JSONArray(value));
+ return this;
+ }
+
+ /**
+ * Put or replace a double value. If the index is greater than the length
+ * of the JSONArray, then null elements will be added as necessary to pad
+ * it out.
+ *
+ * @param index The subscript.
+ * @param value A double value.
+ * @return this.
+ * @throws JSONException If the index is negative or if the value is not
+ * finite.
+ */
+ public JSONArray put(int index, double value) throws JSONException {
+ this.put(index, new Double(value));
+ return this;
+ }
+
+ /**
+ * Put or replace an int value. If the index is greater than the length of the JSONArray, then null elements will be
+ * added as necessary to pad it out.
+ *
+ * @param index The subscript.
+ * @param value An int value.
+ * @return this.
+ * @throws JSONException If the index is negative.
+ */
+ public JSONArray put(int index, int value) throws JSONException {
+ this.put(index, Integer.valueOf(value));
+ return this;
+ }
+
+ /**
+ * Put or replace a long value. If the index is greater than the length of the JSONArray, then null elements will be
+ * added as necessary to pad it out.
+ *
+ * @param index The subscript.
+ * @param value A long value.
+ * @return this.
+ * @throws JSONException If the index is negative.
+ */
+ public JSONArray put(int index, long value) throws JSONException {
+ this.put(index, Long.valueOf(value));
+ return this;
+ }
+
+ /**
+ * Put a value in the JSONArray, where the value will be a JSONObject that
+ * is produced from a Map.
+ *
+ * @param index The subscript.
+ * @param value The Map value.
+ * @return this.
+ * @throws JSONException If the index is negative or if the the value is an
+ * invalid number.
+ */
+ public JSONArray put(int index, Map value) throws JSONException {
+ this.put(index, new JSONObject(value));
+ return this;
+ }
+
+ /**
+ * Put or replace an object value in the JSONArray. If the index is greater than the length of the JSONArray, then
+ * null elements will be added as necessary to pad it out.
+ *
+ * @param index The subscript.
+ * @param value The value to put into the array. The value should be a Boolean, Double, Integer, JSONArray,
+ * JSONObject, Long, or String, or the JSONObject.NULL object.
+ * @return this.
+ * @throws JSONException If the index is negative or if the the value is an invalid number.
+ */
+ public JSONArray put(int index, Object value) throws JSONException {
+ JSONObject.testValidity(value);
+ if (index < 0) {
+ throw new JSONException("JSONArray[" + index + "] not found.");
+ }
+ if (index < length()) {
+ this.myArrayList.set(index, value);
+ } else {
+ while (index != length()) {
+ this.put(JSONObject.NULL);
+ }
+ this.put(value);
+ }
+ return this;
+ }
+
+ /**
+ * Remove an index and close the hole.
+ *
+ * @param index The index of the element to be removed.
+ * @return The value that was associated with the index, or null if there was no value.
+ */
+ public Object remove(int index) {
+ return (index >= 0) && (index < length()) ? this.myArrayList.remove(index) : null;
+ }
+
+ /**
+ * Determine if two JSONArrays are similar. They must contain similar sequences.
+ *
+ * @param other The other JSONArray
+ * @return true if they are equal
+ */
+ public boolean similar(Object other) {
+ if (!(other instanceof JSONArray)) {
+ return false;
+ }
+ int len = length();
+ if (len != ((JSONArray) other).length()) {
+ return false;
+ }
+ for (int i = 0; i < len; i += 1) {
+ Object valueThis = get(i);
+ Object valueOther = ((JSONArray) other).get(i);
+ if (valueThis instanceof JSONObject) {
+ if (!((JSONObject) valueThis).similar(valueOther)) {
+ return false;
+ }
+ } else if (valueThis instanceof JSONArray) {
+ if (!((JSONArray) valueThis).similar(valueOther)) {
+ return false;
+ }
+ } else if (!valueThis.equals(valueOther)) {
+ return false;
+ }
+ }
+ return true;
+ }
+
+ /**
+ * Produce a JSONObject by combining a JSONArray of names with the values of this JSONArray.
+ *
+ * @param names A JSONArray containing a list of key strings. These will be paired with the values.
+ * @return A JSONObject, or null if there are no names or if this JSONArray has no values.
+ * @throws JSONException If any of the names are null.
+ */
+ public JSONObject toJSONObject(JSONArray names) throws JSONException {
+ if ((names == null) || (names.length() == 0) || (length() == 0)) {
+ return null;
+ }
+ JSONObject jo = new JSONObject();
+ for (int i = 0; i < names.length(); i += 1) {
+ jo.put(names.getString(i), opt(i));
+ }
+ return jo;
+ }
+
+ /**
+ * Make a JSON text of this JSONArray. For compactness, no unnecessary whitespace is added. If it is not possible to
+ * produce a syntactically correct JSON text then null will be returned instead. This could occur if the array
+ * contains an invalid number.
+ *
+ * Warning: This method assumes that the data structure is acyclical.
+ *
+ * @return a printable, displayable, transmittable representation of the array.
+ */
+ @Override public String toString() {
+ try {
+ return this.toString(0);
+ } catch (JSONException ignored) {
+ return null;
+ }
+ }
+
+ /**
+ * Make a prettyprinted JSON text of this JSONArray. Warning: This method assumes that the data structure is
+ * acyclical.
+ *
+ * @param indentFactor The number of spaces to add to each level of indentation.
+ * @return a printable, displayable, transmittable representation of the object, beginning with
+ * {@code [} (left bracket) and ending with {@code ]} (right
+ * bracket) .
+ * @throws JSONException
+ */
+ public String toString(int indentFactor) throws JSONException {
+ StringWriter sw = new StringWriter();
+ synchronized (sw.getBuffer()) {
+ return this.write(sw, indentFactor, 0).toString();
+ }
+ }
+
+ /**
+ * Write the contents of the JSONArray as JSON text to a writer. For compactness, no whitespace is added.
+ *
+ * Warning: This method assumes that the data structure is acyclical.
+ *
+ * @return The writer.
+ * @throws JSONException
+ */
+ public Writer write(Writer writer) throws JSONException {
+ return this.write(writer, 0, 0);
+ }
+
+ /**
+ * Write the contents of the JSONArray as JSON text to a writer. For compactness, no whitespace is added.
+ *
+ * Warning: This method assumes that the data structure is acyclical.
+ *
+ * @param indentFactor The number of spaces to add to each level of indentation.
+ * @param indent The indention of the top level.
+ * @return The writer.
+ * @throws JSONException
+ */
+ Writer write(Writer writer, int indentFactor, int indent) throws JSONException {
+ try {
+ boolean commanate = false;
+ int length = length();
+ writer.write('[');
+ if (length == 1) {
+ JSONObject.writeValue(writer, this.myArrayList.get(0), indentFactor, indent);
+ } else if (length != 0) {
+ int newindent = indent + indentFactor;
+ for (int i = 0; i < length; i += 1) {
+ if (commanate) {
+ writer.write(',');
+ }
+ if (indentFactor > 0) {
+ writer.write('\n');
+ }
+ JSONObject.indent(writer, newindent);
+ JSONObject.writeValue(writer, this.myArrayList.get(i), indentFactor, newindent);
+ commanate = true;
+ }
+ if (indentFactor > 0) {
+ writer.write('\n');
+ }
+ JSONObject.indent(writer, indent);
+ }
+ writer.write(']');
+ return writer;
+ } catch (IOException e) {
+ throw new JSONException(e);
+ }
+ }
+}
diff --git a/Bukkit/src/main/java/com/github/intellectualsites/plotsquared/json/JSONException.java b/Bukkit/src/main/java/com/github/intellectualsites/plotsquared/json/JSONException.java
new file mode 100644
index 000000000..67de10815
--- /dev/null
+++ b/Bukkit/src/main/java/com/github/intellectualsites/plotsquared/json/JSONException.java
@@ -0,0 +1,40 @@
+package com.github.intellectualsites.plotsquared.json;
+
+/**
+ * The JSONException is thrown by the JSON.org classes when things are amiss.
+ *
+ * @author JSON.org
+ * @version 2014-05-03
+ */
+public class JSONException extends RuntimeException {
+ private static final long serialVersionUID = 0;
+ private Throwable cause;
+
+ /**
+ * Constructs a JSONException with an explanatory message.
+ *
+ * @param message Detail about the reason for the exception.
+ */
+ public JSONException(final String message) {
+ super(message);
+ }
+
+ /**
+ * Constructs a new JSONException with the specified cause.
+ *
+ * @param cause The cause.
+ */
+ public JSONException(final Throwable cause) {
+ super(cause.getMessage());
+ this.cause = cause;
+ }
+
+ /**
+ * Returns the cause of this exception or null if the cause is nonexistent or unknown.
+ *
+ * @return the cause of this exception or null if the cause is nonexistent or unknown.
+ */
+ @Override public Throwable getCause() {
+ return cause;
+ }
+}
diff --git a/Bukkit/src/main/java/com/github/intellectualsites/plotsquared/json/JSONObject.java b/Bukkit/src/main/java/com/github/intellectualsites/plotsquared/json/JSONObject.java
new file mode 100644
index 000000000..9a84920dc
--- /dev/null
+++ b/Bukkit/src/main/java/com/github/intellectualsites/plotsquared/json/JSONObject.java
@@ -0,0 +1,1425 @@
+package com.github.intellectualsites.plotsquared.json;
+
+import java.io.IOException;
+import java.io.StringWriter;
+import java.io.Writer;
+import java.lang.reflect.Field;
+import java.lang.reflect.InvocationTargetException;
+import java.lang.reflect.Method;
+import java.lang.reflect.Modifier;
+import java.util.*;
+import java.util.Map.Entry;
+
+/**
+ * A JSONObject is an unordered collection of name/value pairs. Its external form is a string wrapped in curly braces
+ * with colons between the names and values, and commas between the values and names. The internal form is an object
+ * having get
and opt
methods for accessing the values by name, and put
methods
+ * for adding or replacing values by name. The values can be any of these types: Boolean
,
+ * JSONArray
, JSONObject
, Number
, String
, or the
+ * JSONObject.NULL
object. A JSONObject constructor can be used to convert an external form JSON text into
+ * an internal form whose values can be retrieved with the get
and opt
methods, or to convert
+ * values into a JSON text using the put
and toString
methods. A get
method
+ * returns a value if one can be found, and throws an exception if one cannot be found. An opt
method
+ * returns a default value instead of throwing an exception, and so is useful for obtaining optional values.
+ *
+ * The generic get()
and opt()
methods return an object, which you can cast or query for type.
+ * There are also typed get
and opt
methods that do type checking and type coercion for you.
+ * The opt methods differ from the get methods in that they do not throw. Instead, they return a specified value, such
+ * as null.
+ *
+ * The put
methods add or replace values in an object. For example,
+ *
+ *
+ *
+ * myString = new JSONObject().put("JSON", "Hello, World!").toString();
+ *
+ *
+ * produces the string {"JSON": "Hello, World"}
.
+ *
+ * The texts produced by the toString
methods strictly conform to the JSON syntax rules. The constructors
+ * are more forgiving in the texts they will accept:
An extra ,
(comma) may
+ * appear just before the closing brace. Strings may be quoted with '
(single
+ * quote) . Strings do not need to be quoted at all if they do not begin with a quote or single quote,
+ * and if they do not contain leading or trailing spaces, and if they do not contain any of these characters: { }
+ * [ ] / \ : , #
and if they do not look like numbers and if they are not the reserved words true
,
+ * false
, or null
.
+ *
+ * @author JSON.org
+ * @version 2014-05-03
+ */
+public class JSONObject {
+ /**
+ * It is sometimes more convenient and less ambiguous to have a NULL
object than to use Java's
+ * null
value. JSONObject.NULL.equals(null)
returns true
.
+ * JSONObject.NULL.toString()
returns "null"
.
+ */
+ public static final Object NULL = new Null();
+ /**
+ * The map where the JSONObject's properties are kept.
+ */
+ private final Map map;
+
+ /**
+ * Construct an empty JSONObject.
+ */
+ public JSONObject() {
+ this.map = new HashMap();
+ }
+
+ /**
+ * Construct a JSONObject from a subset of another JSONObject. An array of strings is used to identify the keys that
+ * should be copied. Missing keys are ignored.
+ *
+ * @param jo A JSONObject.
+ * @param names An array of strings.
+ * @throws JSONException
+ * @throws JSONException If a value is a non-finite number or if a name is duplicated.
+ */
+ public JSONObject(JSONObject jo, String[] names) {
+ this();
+ for (String name : names) {
+ try {
+ putOnce(name, jo.opt(name));
+ } catch (JSONException ignore) {
+ }
+ }
+ }
+
+ /**
+ * Construct a JSONObject from a JSONTokener.
+ *
+ * @param x A JSONTokener object containing the source string.
+ * @throws JSONException If there is a syntax error in the source string or a duplicated key.
+ */
+ public JSONObject(JSONTokener x) throws JSONException {
+ this();
+ char c;
+ String key;
+ if (x.nextClean() != '{') {
+ throw x.syntaxError("A JSONObject text must begin with '{'");
+ }
+ for (; ; ) {
+ c = x.nextClean();
+ switch (c) {
+ case 0:
+ throw x.syntaxError("A JSONObject text must end with '}'");
+ case '}':
+ return;
+ default:
+ x.back();
+ key = x.nextValue().toString();
+ }
+ // The key is followed by ':'.
+ c = x.nextClean();
+ if (c != ':') {
+ throw x.syntaxError("Expected a ':' after a key");
+ }
+ putOnce(key, x.nextValue());
+ // Pairs are separated by ','.
+ switch (x.nextClean()) {
+ case ';':
+ case ',':
+ if (x.nextClean() == '}') {
+ return;
+ }
+ x.back();
+ break;
+ case '}':
+ return;
+ default:
+ throw x.syntaxError("Expected a ',' or '}'");
+ }
+ }
+ }
+
+ /**
+ * Construct a JSONObject from a Map.
+ *
+ * @param map A map object that can be used to initialize the contents of the JSONObject.
+ * @throws JSONException
+ */
+ public JSONObject(Map map) {
+ this.map = new HashMap();
+ if (map != null) {
+ for (Entry entry : map.entrySet()) {
+ Object value = entry.getValue();
+ if (value != null) {
+ this.map.put(entry.getKey(), wrap(value));
+ }
+ }
+ }
+ }
+
+ /**
+ * Construct a JSONObject from an Object using bean getters. It reflects on all of the public methods of the object.
+ * For each of the methods with no parameters and a name starting with "get"
or "is"
+ * followed by an uppercase letter, the method is invoked, and a key and the value returned from the getter method
+ * are put into the new JSONObject.
+ *
+ * The key is formed by removing the "get"
or "is"
prefix. If the second remaining
+ * character is not upper case, then the first character is converted to lower case.
+ *
+ * For example, if an object has a method named "getPluginName"
, and if the result of calling
+ * object.getPluginName()
is "Larry Fine"
, then the JSONObject will contain "name": "Larry
+ * Fine"
.
+ *
+ * @param bean An object that has getter methods that should be used to make a JSONObject.
+ */
+ public JSONObject(Object bean) {
+ this();
+ populateMap(bean);
+ }
+
+ /**
+ * Construct a JSONObject from an Object, using reflection to find the public members. The resulting JSONObject's
+ * keys will be the strings from the names array, and the values will be the field values associated with those keys
+ * in the object. If a key is not found or not visible, then it will not be copied into the new JSONObject.
+ *
+ * @param object An object that has fields that should be used to make a JSONObject.
+ * @param names An array of strings, the names of the fields to be obtained from the object.
+ */
+ public JSONObject(Object object, String names[]) {
+ this();
+ Class c = object.getClass();
+ for (String name : names) {
+ try {
+ putOpt(name, c.getField(name).get(object));
+ } catch (JSONException | SecurityException | NoSuchFieldException | IllegalArgumentException | IllegalAccessException ignore) {
+ }
+ }
+ }
+
+ /**
+ * Construct a JSONObject from a source JSON text string. This is the most commonly used JSONObject constructor.
+ *
+ * @param source A string beginning with {
(left brace) and ending with
+ * }
(right brace) .
+ * @throws JSONException If there is a syntax error in the source string or a duplicated key.
+ */
+ public JSONObject(String source) throws JSONException {
+ this(new JSONTokener(source));
+ }
+
+ /**
+ * Construct a JSONObject from a ResourceBundle.
+ *
+ * @param baseName The ResourceBundle base name.
+ * @param locale The Locale to load the ResourceBundle for.
+ * @throws JSONException If any JSONExceptions are detected.
+ */
+ public JSONObject(String baseName, Locale locale) throws JSONException {
+ this();
+ ResourceBundle bundle = ResourceBundle
+ .getBundle(baseName, locale, Thread.currentThread().getContextClassLoader());
+ // Iterate through the keys in the bundle.
+ Enumeration keys = bundle.getKeys();
+ while (keys.hasMoreElements()) {
+ Object key = keys.nextElement();
+ if (key != null) {
+ // Go through the path, ensuring that there is a nested
+ // JSONObject for each
+ // segment except the last. Add the value using the last
+ // segment's name into
+ // the deepest nested JSONObject.
+ String[] path = ((String) key).split("\\.");
+ int last = path.length - 1;
+ JSONObject target = this;
+ for (int i = 0; i < last; i += 1) {
+ String segment = path[i];
+ JSONObject nextTarget = target.optJSONObject(segment);
+ if (nextTarget == null) {
+ nextTarget = new JSONObject();
+ target.put(segment, nextTarget);
+ }
+ target = nextTarget;
+ }
+ target.put(path[last], bundle.getString((String) key));
+ }
+ }
+ }
+
+ /**
+ * Produce a string from a double. The string "null" will be returned if the number is not finite.
+ *
+ * @param d A double.
+ * @return A String.
+ */
+ public static String doubleToString(double d) {
+ if (Double.isInfinite(d) || Double.isNaN(d)) {
+ return "null";
+ }
+ // Shave off trailing zeros and decimal point, if possible.
+ String string = Double.toString(d);
+ if ((string.indexOf('.') > 0) && (string.indexOf('e') < 0) && (string.indexOf('E') < 0)) {
+ while (string.endsWith("0")) {
+ string = string.substring(0, string.length() - 1);
+ }
+ if (string.endsWith(".")) {
+ string = string.substring(0, string.length() - 1);
+ }
+ }
+ return string;
+ }
+
+ /**
+ * Get an array of field names from a JSONObject.
+ *
+ * @return An array of field names, or null if there are no names.
+ */
+ public static String[] getNames(JSONObject jo) {
+ int length = jo.length();
+ if (length == 0) {
+ return null;
+ }
+ Iterator iterator = jo.keys();
+ String[] names = new String[length];
+ int i = 0;
+ while (iterator.hasNext()) {
+ names[i] = iterator.next();
+ i += 1;
+ }
+ return names;
+ }
+
+ /**
+ * Get an array of field names from an Object.
+ *
+ * @return An array of field names, or null if there are no names.
+ */
+ public static String[] getNames(Object object) {
+ if (object == null) {
+ return null;
+ }
+ Class klass = object.getClass();
+ Field[] fields = klass.getFields();
+ int length = fields.length;
+ if (length == 0) {
+ return null;
+ }
+ String[] names = new String[length];
+ for (int i = 0; i < length; i += 1) {
+ names[i] = fields[i].getName();
+ }
+ return names;
+ }
+
+ /**
+ * Produce a string from a Number.
+ *
+ * @param number A Number
+ * @return A String.
+ * @throws JSONException If n is a non-finite number.
+ */
+ public static String numberToString(Number number) throws JSONException {
+ if (number == null) {
+ throw new JSONException("Null pointer");
+ }
+ testValidity(number);
+ // Shave off trailing zeros and decimal point, if possible.
+ String string = number.toString();
+ if ((string.indexOf('.') > 0) && (string.indexOf('e') < 0) && (string.indexOf('E') < 0)) {
+ while (string.endsWith("0")) {
+ string = string.substring(0, string.length() - 1);
+ }
+ if (string.endsWith(".")) {
+ string = string.substring(0, string.length() - 1);
+ }
+ }
+ return string;
+ }
+
+ /**
+ * Produce a string in double quotes with backslash sequences in all the right places. A backslash will be inserted
+ * control character or an unescaped quote or backslash.
+ *
+ * @param string A String
+ * @return A String correctly formatted for insertion in a JSON text.
+ */
+ public static String quote(String string) {
+ StringWriter sw = new StringWriter();
+ synchronized (sw.getBuffer()) {
+ try {
+ return quote(string, sw).toString();
+ } catch (IOException ignored) {
+ // will never happen - we are writing to a string writer
+ return "";
+ }
+ }
+ }
+
+ public static Writer quote(String string, Writer w) throws IOException {
+ if ((string == null) || string.isEmpty()) {
+ w.write("\"\"");
+ return w;
+ }
+ char b;
+ char c = 0;
+ String hhhh;
+ int i;
+ int len = string.length();
+ w.write('"');
+ for (i = 0; i < len; i += 1) {
+ b = c;
+ c = string.charAt(i);
+ switch (c) {
+ case '\\':
+ case '"':
+ w.write('\\');
+ w.write(c);
+ break;
+ case '/':
+ if (b == '<') {
+ w.write('\\');
+ }
+ w.write(c);
+ break;
+ case '\b':
+ w.write("\\b");
+ break;
+ case '\t':
+ w.write("\\t");
+ break;
+ case '\n':
+ w.write("\\n");
+ break;
+ case '\f':
+ w.write("\\f");
+ break;
+ case '\r':
+ w.write("\\r");
+ break;
+ default:
+ if ((c < ' ') || ((c >= '\u0080') && (c < '\u00a0')) || ((c >= '\u2000') && (c
+ < '\u2100'))) {
+ w.write("\\u");
+ hhhh = Integer.toHexString(c);
+ w.write("0000", 0, 4 - hhhh.length());
+ w.write(hhhh);
+ } else {
+ w.write(c);
+ }
+ }
+ }
+ w.write('"');
+ return w;
+ }
+
+ /**
+ * Try to convert a string into a number, boolean, or null. If the string can't be converted, return the string.
+ *
+ * @param string A String.
+ * @return A simple JSON value.
+ */
+ public static Object stringToValue(String string) {
+ Double d;
+ if (string.isEmpty()) {
+ return string;
+ }
+ if (string.equalsIgnoreCase("true")) {
+ return Boolean.TRUE;
+ }
+ if (string.equalsIgnoreCase("false")) {
+ return Boolean.FALSE;
+ }
+ if (string.equalsIgnoreCase("null")) {
+ return JSONObject.NULL;
+ }
+ /*
+ * If it might be a number, try converting it. If a number cannot be
+ * produced, then the value will just be a string.
+ */
+ char b = string.charAt(0);
+ if (((b >= '0') && (b <= '9')) || (b == '-')) {
+ try {
+ if ((string.indexOf('.') > -1) || (string.indexOf('e') > -1) || (string.indexOf('E')
+ > -1)) {
+ d = Double.valueOf(string);
+ if (!d.isInfinite() && !d.isNaN()) {
+ return d;
+ }
+ } else {
+ Long myLong = Long.valueOf(string);
+ if (string.equals(myLong.toString())) {
+ if (myLong == myLong.intValue()) {
+ return myLong.intValue();
+ } else {
+ return myLong;
+ }
+ }
+ }
+ } catch (NumberFormatException ignore) {
+ }
+ }
+ return string;
+ }
+
+ /**
+ * Throw an exception if the object is a NaN or infinite number.
+ *
+ * @param o The object to test.
+ * @throws JSONException If o is a non-finite number.
+ */
+ public static void testValidity(Object o) throws JSONException {
+ if (o != null) {
+ if (o instanceof Double) {
+ if (((Double) o).isInfinite() || ((Double) o).isNaN()) {
+ throw new JSONException("JSON does not allow non-finite numbers.");
+ }
+ } else if (o instanceof Float) {
+ if (((Float) o).isInfinite() || ((Float) o).isNaN()) {
+ throw new JSONException("JSON does not allow non-finite numbers.");
+ }
+ }
+ }
+ }
+
+ /**
+ * Make a JSON text of an Object value. If the object has an value.toJSONString() method, then that method will be
+ * used to produce the JSON text. The method is required to produce a strictly conforming text. If the object does
+ * not contain a toJSONString method (which is the most common case), then a text will be produced by other means.
+ * If the value is an array or Collection, then a JSONArray will be made from it and its toJSONString method will be
+ * called. If the value is a MAP, then a JSONObject will be made from it and its toJSONString method will be called.
+ * Otherwise, the value's toString method will be called, and the result will be quoted.
+ *
+ *
+ * Warning: This method assumes that the data structure is acyclical.
+ *
+ * @param value The value to be serialized.
+ * @return a printable, displayable, transmittable representation of the object, beginning with
+ * {
(left brace) and ending with }
(right
+ * brace) .
+ * @throws JSONException If the value is or contains an invalid number.
+ */
+ public static String valueToString(Object value) throws JSONException {
+ if ((value == null) || value.equals(null)) {
+ return "null";
+ }
+ if (value instanceof JSONString) {
+ Object object;
+ try {
+ object = ((JSONString) value).toJSONString();
+ } catch (Exception e) {
+ throw new JSONException(e);
+ }
+ if (object != null) {
+ return (String) object;
+ }
+ throw new JSONException("Bad value from toJSONString: " + object);
+ }
+ if (value instanceof Number) {
+ return numberToString((Number) value);
+ }
+ if ((value instanceof Boolean) || (value instanceof JSONObject)
+ || (value instanceof JSONArray)) {
+ return value.toString();
+ }
+ if (value instanceof Map) {
+ return new JSONObject((Map) value).toString();
+ }
+ if (value instanceof Collection) {
+ return new JSONArray((Collection) value).toString();
+ }
+ if (value.getClass().isArray()) {
+ return new JSONArray(value).toString();
+ }
+ return quote(value.toString());
+ }
+
+ /**
+ * Wrap an object, if necessary. If the object is null, return the NULL object. If it is an array or collection,
+ * wrap it in a JSONArray. If it is a map, wrap it in a JSONObject. If it is a standard property (Double, String, et
+ * al) then it is already wrapped. Otherwise, if it comes from one of the java packages, turn it into a string. And
+ * if it doesn't, try to wrap it in a JSONObject. If the wrapping fails, then null is returned.
+ *
+ * @param object The object to wrap
+ * @return The wrapped value
+ */
+ public static Object wrap(Object object) {
+ try {
+ if (object == null) {
+ return NULL;
+ }
+ if ((object instanceof JSONObject) || (object instanceof JSONArray) || NULL
+ .equals(object) || (object instanceof JSONString) || (object instanceof Byte)
+ || (object instanceof Character) || (object instanceof Short)
+ || (object instanceof Integer) || (object instanceof Long)
+ || (object instanceof Boolean) || (object instanceof Float)
+ || (object instanceof Double) || (object instanceof String)) {
+ return object;
+ }
+ if (object instanceof Collection) {
+ return new JSONArray((Collection) object);
+ }
+ if (object.getClass().isArray()) {
+ return new JSONArray(object);
+ }
+ if (object instanceof Map) {
+ return new JSONObject((Map) object);
+ }
+ Package objectPackage = object.getClass().getPackage();
+ String objectPackageName = objectPackage != null ? objectPackage.getName() : "";
+ if (objectPackageName.startsWith("java.") || objectPackageName.startsWith("javax.") || (
+ object.getClass().getClassLoader() == null)) {
+ return object.toString();
+ }
+ return new JSONObject(object);
+ } catch (JSONException ignored) {
+ return null;
+ }
+ }
+
+ static final Writer writeValue(Writer writer, Object value, int indentFactor, int indent)
+ throws JSONException, IOException {
+ if ((value == null) || value.equals(null)) {
+ writer.write("null");
+ } else if (value instanceof JSONObject) {
+ ((JSONObject) value).write(writer, indentFactor, indent);
+ } else if (value instanceof JSONArray) {
+ ((JSONArray) value).write(writer, indentFactor, indent);
+ } else if (value instanceof Map) {
+ new JSONObject((Map) value).write(writer, indentFactor, indent);
+ } else if (value instanceof Collection) {
+ new JSONArray((Collection) value).write(writer, indentFactor, indent);
+ } else if (value.getClass().isArray()) {
+ new JSONArray(value).write(writer, indentFactor, indent);
+ } else if (value instanceof Number) {
+ writer.write(numberToString((Number) value));
+ } else if (value instanceof Boolean) {
+ writer.write(value.toString());
+ } else if (value instanceof JSONString) {
+ Object o;
+ try {
+ o = ((JSONString) value).toJSONString();
+ } catch (Exception e) {
+ throw new JSONException(e);
+ }
+ writer.write(o != null ? o.toString() : quote(value.toString()));
+ } else {
+ quote(value.toString(), writer);
+ }
+ return writer;
+ }
+
+ static final void indent(Writer writer, int indent) throws IOException {
+ for (int i = 0; i < indent; i += 1) {
+ writer.write(' ');
+ }
+ }
+
+ /**
+ * Accumulate values under a key. It is similar to the put method except that if there is already an object stored
+ * under the key then a JSONArray is stored under the key to hold all of the accumulated values. If there is already
+ * a JSONArray, then the new value is appended to it. In contrast, the put method replaces the previous value.
+ *
+ * If only one value is accumulated that is not a JSONArray, then the result will be the same as using put. But if
+ * multiple values are accumulated, then the result will be like append.
+ *
+ * @param key A key string.
+ * @param value An object to be accumulated under the key.
+ * @return this.
+ * @throws JSONException If the value is an invalid number or if the key is null.
+ */
+ public JSONObject accumulate(String key, Object value) throws JSONException {
+ testValidity(value);
+ Object object = opt(key);
+ if (object == null) {
+ this.put(key, value instanceof JSONArray ? new JSONArray().put(value) : value);
+ } else if (object instanceof JSONArray) {
+ ((JSONArray) object).put(value);
+ } else {
+ this.put(key, new JSONArray().put(object).put(value));
+ }
+ return this;
+ }
+
+ /**
+ * Append values to the array under a key. If the key does not exist in the JSONObject, then the key is put in the
+ * JSONObject with its value being a JSONArray containing the value parameter. If the key was already associated
+ * with a JSONArray, then the value parameter is appended to it.
+ *
+ * @param key A key string.
+ * @param value An object to be accumulated under the key.
+ * @return this.
+ * @throws JSONException If the key is null or if the current value associated with the key is not a JSONArray.
+ */
+ public JSONObject append(String key, Object value) throws JSONException {
+ testValidity(value);
+ Object object = opt(key);
+ if (object == null) {
+ this.put(key, new JSONArray().put(value));
+ } else if (object instanceof JSONArray) {
+ this.put(key, ((JSONArray) object).put(value));
+ } else {
+ throw new JSONException("JSONObject[" + key + "] is not a JSONArray.");
+ }
+ return this;
+ }
+
+ /**
+ * Get the value object associated with a key.
+ *
+ * @param key A key string.
+ * @return The object associated with the key.
+ * @throws JSONException if the key is not found.
+ */
+ public Object get(String key) throws JSONException {
+ if (key == null) {
+ throw new JSONException("Null key.");
+ }
+ Object object = opt(key);
+ if (object == null) {
+ throw new JSONException("JSONObject[" + quote(key) + "] not found.");
+ }
+ return object;
+ }
+
+ /**
+ * Get the boolean value associated with a key.
+ *
+ * @param key A key string.
+ * @return The truth.
+ * @throws JSONException if the value is not a Boolean or the String "true" or "false".
+ */
+ public boolean getBoolean(String key) throws JSONException {
+ Object object = get(key);
+ if (object.equals(Boolean.FALSE) || ((object instanceof String) && ((String) object)
+ .equalsIgnoreCase("false"))) {
+ return false;
+ } else if (object.equals(Boolean.TRUE) || ((object instanceof String) && ((String) object)
+ .equalsIgnoreCase("true"))) {
+ return true;
+ }
+ throw new JSONException("JSONObject[" + quote(key) + "] is not a Boolean.");
+ }
+
+ /**
+ * Get the double value associated with a key.
+ *
+ * @param key A key string.
+ * @return The numeric value.
+ * @throws JSONException if the key is not found or if the value is not a Number object and cannot be converted to a
+ * number.
+ */
+ public double getDouble(String key) throws JSONException {
+ Object object = get(key);
+ try {
+ return object instanceof Number ?
+ ((Number) object).doubleValue() :
+ Double.parseDouble((String) object);
+ } catch (NumberFormatException ignored) {
+ throw new JSONException("JSONObject[" + quote(key) + "] is not a number.");
+ }
+ }
+
+ /**
+ * Get the int value associated with a key.
+ *
+ * @param key A key string.
+ * @return The integer value.
+ * @throws JSONException if the key is not found or if the value cannot be converted to an integer.
+ */
+ public int getInt(String key) throws JSONException {
+ Object object = get(key);
+ try {
+ return object instanceof Number ?
+ ((Number) object).intValue() :
+ Integer.parseInt((String) object);
+ } catch (NumberFormatException ignored) {
+ throw new JSONException("JSONObject[" + quote(key) + "] is not an int.");
+ }
+ }
+
+ /**
+ * Get the JSONArray value associated with a key.
+ *
+ * @param key A key string.
+ * @return A JSONArray which is the value.
+ * @throws JSONException if the key is not found or if the value is not a JSONArray.
+ */
+ public JSONArray getJSONArray(String key) throws JSONException {
+ Object object = get(key);
+ if (object instanceof JSONArray) {
+ return (JSONArray) object;
+ }
+ throw new JSONException("JSONObject[" + quote(key) + "] is not a JSONArray.");
+ }
+
+ /**
+ * Get the JSONObject value associated with a key.
+ *
+ * @param key A key string.
+ * @return A JSONObject which is the value.
+ * @throws JSONException if the key is not found or if the value is not a JSONObject.
+ */
+ public JSONObject getJSONObject(String key) throws JSONException {
+ Object object = get(key);
+ if (object instanceof JSONObject) {
+ return (JSONObject) object;
+ }
+ throw new JSONException("JSONObject[" + quote(key) + "] is not a JSONObject.");
+ }
+
+ /**
+ * Get the long value associated with a key.
+ *
+ * @param key A key string.
+ * @return The long value.
+ * @throws JSONException if the key is not found or if the value cannot be converted to a long.
+ */
+ public long getLong(String key) throws JSONException {
+ Object object = get(key);
+ try {
+ return object instanceof Number ?
+ ((Number) object).longValue() :
+ Long.parseLong((String) object);
+ } catch (NumberFormatException ignored) {
+ throw new JSONException("JSONObject[" + quote(key) + "] is not a long.");
+ }
+ }
+
+ /**
+ * Get the string associated with a key.
+ *
+ * @param key A key string.
+ * @return A string which is the value.
+ * @throws JSONException if there is no string value for the key.
+ */
+ public String getString(String key) throws JSONException {
+ Object object = get(key);
+ if (object instanceof String) {
+ return (String) object;
+ }
+ throw new JSONException("JSONObject[" + quote(key) + "] not a string.");
+ }
+
+ /**
+ * Determine if the JSONObject contains a specific key.
+ *
+ * @param key A key string.
+ * @return true if the key exists in the JSONObject.
+ */
+ public boolean has(String key) {
+ return this.map.containsKey(key);
+ }
+
+ /**
+ * Increment a property of a JSONObject. If there is no such property, create one with a value of 1. If there is
+ * such a property, and if it is an Integer, Long, Double, or Float, then add one to it.
+ *
+ * @param key A key string.
+ * @return this.
+ * @throws JSONException If there is already a property with this name that is not an Integer, Long, Double, or
+ * Float.
+ */
+ public JSONObject increment(String key) throws JSONException {
+ Object value = opt(key);
+ if (value == null) {
+ this.put(key, 1);
+ } else if (value instanceof Integer) {
+ this.put(key, (Integer) value + 1);
+ } else if (value instanceof Long) {
+ this.put(key, (Long) value + 1);
+ } else if (value instanceof Double) {
+ this.put(key, (Double) value + 1);
+ } else if (value instanceof Float) {
+ this.put(key, (Float) value + 1);
+ } else {
+ throw new JSONException("Unable to increment [" + quote(key) + "].");
+ }
+ return this;
+ }
+
+ /**
+ * Determine if the value associated with the key is null or if there is no value.
+ *
+ * @param key A key string.
+ * @return true if there is no value associated with the key or if the value is the JSONObject.NULL object.
+ */
+ public boolean isNull(String key) {
+ return JSONObject.NULL.equals(opt(key));
+ }
+
+ /**
+ * Get an enumeration of the keys of the JSONObject.
+ *
+ * @return An iterator of the keys.
+ */
+ public Iterator keys() {
+ return keySet().iterator();
+ }
+
+ /**
+ * Get a set of keys of the JSONObject.
+ *
+ * @return A keySet.
+ */
+ public Set keySet() {
+ return this.map.keySet();
+ }
+
+ /**
+ * Get the number of keys stored in the JSONObject.
+ *
+ * @return The number of keys in the JSONObject.
+ */
+ public int length() {
+ return this.map.size();
+ }
+
+ /**
+ * Produce a JSONArray containing the names of the elements of this JSONObject.
+ *
+ * @return A JSONArray containing the key strings, or null if the JSONObject is empty.
+ */
+ public JSONArray names() {
+ JSONArray ja = new JSONArray();
+ Iterator keys = keys();
+ while (keys.hasNext()) {
+ ja.put(keys.next());
+ }
+ return ja.length() == 0 ? null : ja;
+ }
+
+ /**
+ * Get an optional value associated with a key.
+ *
+ * @param key A key string.
+ * @return An object which is the value, or null if there is no value.
+ */
+ public Object opt(String key) {
+ return key == null ? null : this.map.get(key);
+ }
+
+ /**
+ * Get an optional boolean associated with a key. It returns false if there is no such key, or if the value is not
+ * Boolean.TRUE or the String "true".
+ *
+ * @param key A key string.
+ * @return The truth.
+ */
+ public boolean optBoolean(String key) {
+ return this.optBoolean(key, false);
+ }
+
+ /**
+ * Get an optional boolean associated with a key. It returns the defaultValue if there is no such key, or if it is
+ * not a Boolean or the String "true" or "false" (case insensitive).
+ *
+ * @param key A key string.
+ * @param defaultValue The default.
+ * @return The truth.
+ */
+ public boolean optBoolean(String key, boolean defaultValue) {
+ try {
+ return getBoolean(key);
+ } catch (JSONException ignored) {
+ return defaultValue;
+ }
+ }
+
+ /**
+ * Get an optional double associated with a key, or NaN if there is no such key or if its value is not a number. If
+ * the value is a string, an attempt will be made to evaluate it as a number.
+ *
+ * @param key A string which is the key.
+ * @return An object which is the value.
+ */
+ public double optDouble(String key) {
+ return this.optDouble(key, Double.NaN);
+ }
+
+ /**
+ * Get an optional double associated with a key, or the defaultValue if there is no such key or if its value is not
+ * a number. If the value is a string, an attempt will be made to evaluate it as a number.
+ *
+ * @param key A key string.
+ * @param defaultValue The default.
+ * @return An object which is the value.
+ */
+ public double optDouble(String key, double defaultValue) {
+ try {
+ return getDouble(key);
+ } catch (JSONException ignored) {
+ return defaultValue;
+ }
+ }
+
+ /**
+ * Get an optional int value associated with a key, or zero if there is no such key or if the value is not a number.
+ * If the value is a string, an attempt will be made to evaluate it as a number.
+ *
+ * @param key A key string.
+ * @return An object which is the value.
+ */
+ public int optInt(String key) {
+ return this.optInt(key, 0);
+ }
+
+ /**
+ * Get an optional int value associated with a key, or the default if there is no such key or if the value is not a
+ * number. If the value is a string, an attempt will be made to evaluate it as a number.
+ *
+ * @param key A key string.
+ * @param defaultValue The default.
+ * @return An object which is the value.
+ */
+ public int optInt(String key, int defaultValue) {
+ try {
+ return getInt(key);
+ } catch (JSONException ignored) {
+ return defaultValue;
+ }
+ }
+
+ /**
+ * Get an optional JSONArray associated with a key. It returns null if there is no such key, or if its value is not
+ * a JSONArray.
+ *
+ * @param key A key string.
+ * @return A JSONArray which is the value.
+ */
+ public JSONArray optJSONArray(String key) {
+ Object o = opt(key);
+ return o instanceof JSONArray ? (JSONArray) o : null;
+ }
+
+ /**
+ * Get an optional JSONObject associated with a key. It returns null if there is no such key, or if its value is not
+ * a JSONObject.
+ *
+ * @param key A key string.
+ * @return A JSONObject which is the value.
+ */
+ public JSONObject optJSONObject(String key) {
+ Object object = opt(key);
+ return object instanceof JSONObject ? (JSONObject) object : null;
+ }
+
+ /**
+ * Get an optional long value associated with a key, or zero if there is no such key or if the value is not a
+ * number. If the value is a string, an attempt will be made to evaluate it as a number.
+ *
+ * @param key A key string.
+ * @return An object which is the value.
+ */
+ public long optLong(String key) {
+ return this.optLong(key, 0);
+ }
+
+ /**
+ * Get an optional long value associated with a key, or the default if there is no such key or if the value is not a
+ * number. If the value is a string, an attempt will be made to evaluate it as a number.
+ *
+ * @param key A key string.
+ * @param defaultValue The default.
+ * @return An object which is the value.
+ */
+ public long optLong(String key, long defaultValue) {
+ try {
+ return getLong(key);
+ } catch (JSONException ignored) {
+ return defaultValue;
+ }
+ }
+
+ /**
+ * Get an optional string associated with a key. It returns an empty string if there is no such key. If the value is
+ * not a string and is not null, then it is converted to a string.
+ *
+ * @param key A key string.
+ * @return A string which is the value.
+ */
+ public String optString(String key) {
+ return this.optString(key, "");
+ }
+
+ /**
+ * Get an optional string associated with a key. It returns the defaultValue if there is no such key.
+ *
+ * @param key A key string.
+ * @param defaultValue The default.
+ * @return A string which is the value.
+ */
+ public String optString(String key, String defaultValue) {
+ Object object = opt(key);
+ return NULL.equals(object) ? defaultValue : object.toString();
+ }
+
+ private void populateMap(Object bean) {
+ Class klass = bean.getClass();
+ // If klass is a System class then set includeSuperClass to false.
+ boolean includeSuperClass = klass.getClassLoader() != null;
+ Method[] methods = includeSuperClass ? klass.getMethods() : klass.getDeclaredMethods();
+ for (Method method : methods) {
+ try {
+ if (Modifier.isPublic(method.getModifiers())) {
+ String name = method.getName();
+ String key = "";
+ if (name.startsWith("get")) {
+ if ("getClass".equals(name) || "getDeclaringClass".equals(name)) {
+ key = "";
+ } else {
+ key = name.substring(3);
+ }
+ } else if (name.startsWith("is")) {
+ key = name.substring(2);
+ }
+ if (!key.isEmpty() && Character.isUpperCase(key.charAt(0)) && (
+ method.getParameterTypes().length == 0)) {
+ if (key.length() == 1) {
+ key = key.toLowerCase();
+ } else if (!Character.isUpperCase(key.charAt(1))) {
+ key = key.substring(0, 1).toLowerCase() + key.substring(1);
+ }
+ Object result = method.invoke(bean, (Object[]) null);
+ if (result != null) {
+ this.map.put(key, wrap(result));
+ }
+ }
+ }
+ } catch (IllegalAccessException | IllegalArgumentException | InvocationTargetException ignore) {
+ }
+ }
+ }
+
+ /**
+ * Put a key/boolean pair in the JSONObject.
+ *
+ * @param key A key string.
+ * @param value A boolean which is the value.
+ * @return this.
+ * @throws JSONException If the key is null.
+ */
+ public JSONObject put(String key, boolean value) throws JSONException {
+ this.put(key, value ? Boolean.TRUE : Boolean.FALSE);
+ return this;
+ }
+
+ /**
+ * Put a key/value pair in the JSONObject, where the value will be a JSONArray which is produced from a Collection.
+ *
+ * @param key A key string.
+ * @param value A Collection value.
+ * @return this.
+ * @throws JSONException
+ */
+ public JSONObject put(String key, Collection value) throws JSONException {
+ this.put(key, new JSONArray(value));
+ return this;
+ }
+
+ /**
+ * Put a key/double pair in the JSONObject.
+ *
+ * @param key A key string.
+ * @param value A double which is the value.
+ * @return this.
+ * @throws JSONException If the key is null or if the number is invalid.
+ */
+ public JSONObject put(String key, double value) throws JSONException {
+ this.put(key, new Double(value));
+ return this;
+ }
+
+ /**
+ * Put a key/int pair in the JSONObject.
+ *
+ * @param key A key string.
+ * @param value An int which is the value.
+ * @return this.
+ * @throws JSONException If the key is null.
+ */
+ public JSONObject put(String key, int value) throws JSONException {
+ this.put(key, Integer.valueOf(value));
+ return this;
+ }
+
+ /**
+ * Put a key/long pair in the JSONObject.
+ *
+ * @param key A key string.
+ * @param value A long which is the value.
+ * @return this.
+ * @throws JSONException If the key is null.
+ */
+ public JSONObject put(String key, long value) throws JSONException {
+ this.put(key, Long.valueOf(value));
+ return this;
+ }
+
+ /**
+ * Put a key/value pair in the JSONObject, where the value will be a JSONObject which is produced from a Map.
+ *
+ * @param key A key string.
+ * @param value A Map value.
+ * @return this.
+ * @throws JSONException
+ */
+ public JSONObject put(String key, Map value) throws JSONException {
+ this.put(key, new JSONObject(value));
+ return this;
+ }
+
+ /**
+ * Put a key/value pair in the JSONObject. If the value is null, then the key will be removed from the JSONObject if
+ * it is present.
+ *
+ * @param key A key string.
+ * @param value An object which is the value. It should be of one of these types: Boolean, Double, Integer,
+ * JSONArray, JSONObject, Long, String, or the JSONObject.NULL object.
+ * @return this.
+ * @throws JSONException If the value is non-finite number or if the key is null.
+ */
+ public JSONObject put(String key, Object value) throws JSONException {
+ if (key == null) {
+ throw new NullPointerException("Null key.");
+ }
+ if (value != null) {
+ testValidity(value);
+ this.map.put(key, value);
+ } else {
+ remove(key);
+ }
+ return this;
+ }
+
+ /**
+ * Put a key/value pair in the JSONObject, but only if the key and the value are both non-null, and only if there is
+ * not already a member with that name.
+ *
+ * @param key string
+ * @param value object
+ * @return this.
+ * @throws JSONException if the key is a duplicate
+ */
+ public JSONObject putOnce(String key, Object value) throws JSONException {
+ if ((key != null) && (value != null)) {
+ if (opt(key) != null) {
+ throw new JSONException("Duplicate key \"" + key + "\"");
+ }
+ this.put(key, value);
+ }
+ return this;
+ }
+
+ /**
+ * Put a key/value pair in the JSONObject, but only if the key and the value are both non-null.
+ *
+ * @param key A key string.
+ * @param value An object which is the value. It should be of one of these types: Boolean, Double, Integer,
+ * JSONArray, JSONObject, Long, String, or the JSONObject.NULL object.
+ * @return this.
+ * @throws JSONException If the value is a non-finite number.
+ */
+ public JSONObject putOpt(String key, Object value) throws JSONException {
+ if ((key != null) && (value != null)) {
+ this.put(key, value);
+ }
+ return this;
+ }
+
+ /**
+ * Remove a name and its value, if present.
+ *
+ * @param key The name to be removed.
+ * @return The value that was associated with the name, or null if there was no value.
+ */
+ public Object remove(String key) {
+ return this.map.remove(key);
+ }
+
+ /**
+ * Determine if two JSONObjects are similar. They must contain the same set of names which must be associated with
+ * similar values.
+ *
+ * @param other The other JSONObject
+ * @return true if they are equal
+ */
+ public boolean similar(Object other) {
+ try {
+ if (!(other instanceof JSONObject)) {
+ return false;
+ }
+ Set set = keySet();
+ if (!set.equals(((JSONObject) other).keySet())) {
+ return false;
+ }
+ for (String name : set) {
+ Object valueThis = get(name);
+ Object valueOther = ((JSONObject) other).get(name);
+ if (valueThis instanceof JSONObject) {
+ if (!((JSONObject) valueThis).similar(valueOther)) {
+ return false;
+ }
+ } else if (valueThis instanceof JSONArray) {
+ if (!((JSONArray) valueThis).similar(valueOther)) {
+ return false;
+ }
+ } else if (!valueThis.equals(valueOther)) {
+ return false;
+ }
+ }
+ return true;
+ } catch (JSONException ignored) {
+ return false;
+ }
+ }
+
+ /**
+ * Produce a JSONArray containing the values of the members of this JSONObject.
+ *
+ * @param names A JSONArray containing a list of key strings. This determines the sequence of the values in the
+ * result.
+ * @return A JSONArray of values.
+ * @throws JSONException If any of the values are non-finite numbers.
+ */
+ public JSONArray toJSONArray(JSONArray names) throws JSONException {
+ if ((names == null) || (names.length() == 0)) {
+ return null;
+ }
+ JSONArray ja = new JSONArray();
+ for (int i = 0; i < names.length(); i += 1) {
+ ja.put(opt(names.getString(i)));
+ }
+ return ja;
+ }
+
+ /**
+ * Make a JSON text of this JSONObject. For compactness, no whitespace is added. If this would not result in a
+ * syntactically correct JSON text, then null will be returned instead.
+ *
+ * Warning: This method assumes that the data structure is acyclical.
+ *
+ * @return a printable, displayable, portable, transmittable representation of the object, beginning with
+ * {
(left brace) and ending with }
(right
+ * brace) .
+ */
+ @Override public String toString() {
+ try {
+ return this.toString(0);
+ } catch (JSONException ignored) {
+ return null;
+ }
+ }
+
+ /**
+ * Make a prettyprinted JSON text of this JSONObject.
+ *
+ * Warning: This method assumes that the data structure is acyclical.
+ *
+ * @param indentFactor The number of spaces to add to each level of indentation.
+ * @return a printable, displayable, portable, transmittable representation of the object, beginning with
+ * {
(left brace) and ending with }
(right
+ * brace) .
+ * @throws JSONException If the object contains an invalid number.
+ */
+ public String toString(int indentFactor) throws JSONException {
+ StringWriter w = new StringWriter();
+ synchronized (w.getBuffer()) {
+ return this.write(w, indentFactor, 0).toString();
+ }
+ }
+
+ /**
+ * Write the contents of the JSONObject as JSON text to a writer. For compactness, no whitespace is added.
+ *
+ * Warning: This method assumes that the data structure is acyclical.
+ *
+ * @return The writer.
+ * @throws JSONException
+ */
+ public Writer write(Writer writer) throws JSONException {
+ return this.write(writer, 0, 0);
+ }
+
+ /**
+ * Write the contents of the JSONObject as JSON text to a writer. For compactness, no whitespace is added.
+ *
+ * Warning: This method assumes that the data structure is acyclical.
+ *
+ * @return The writer.
+ * @throws JSONException
+ */
+ Writer write(Writer writer, int indentFactor, int indent) throws JSONException {
+ try {
+ boolean commanate = false;
+ int length = length();
+ Iterator keys = keys();
+ writer.write('{');
+ if (length == 1) {
+ Object key = keys.next();
+ writer.write(quote(key.toString()));
+ writer.write(':');
+ if (indentFactor > 0) {
+ writer.write(' ');
+ }
+ writeValue(writer, this.map.get(key), indentFactor, indent);
+ } else if (length != 0) {
+ int newindent = indent + indentFactor;
+ while (keys.hasNext()) {
+ Object key = keys.next();
+ if (commanate) {
+ writer.write(',');
+ }
+ if (indentFactor > 0) {
+ writer.write('\n');
+ }
+ indent(writer, newindent);
+ writer.write(quote(key.toString()));
+ writer.write(':');
+ if (indentFactor > 0) {
+ writer.write(' ');
+ }
+ writeValue(writer, this.map.get(key), indentFactor, newindent);
+ commanate = true;
+ }
+ if (indentFactor > 0) {
+ writer.write('\n');
+ }
+ indent(writer, indent);
+ }
+ writer.write('}');
+ return writer;
+ } catch (IOException exception) {
+ throw new JSONException(exception);
+ }
+ }
+
+ /**
+ * JSONObject.NULL is equivalent to the value that JavaScript calls null, whilst Java's null is equivalent to the
+ * value that JavaScript calls undefined.
+ */
+ private static final class Null {
+ /**
+ * There is only intended to be a single instance of the NULL object, so the clone method returns itself.
+ *
+ * @return NULL.
+ */
+ @Override protected final Object clone() {
+ try {
+ return super.clone();
+ } catch (CloneNotSupportedException ignored) {
+ return this;
+ }
+ }
+
+ /**
+ * A Null object is equal to the null value and to itself.
+ *
+ * @param object An object to test for nullness.
+ * @return true if the object parameter is the JSONObject.NULL object or null.
+ */
+ @Override public boolean equals(Object object) {
+ return (object == null) || (object == this);
+ }
+
+ /**
+ * Get the "null" string value.
+ *
+ * @return The string "null".
+ */
+ @Override public String toString() {
+ return "null";
+ }
+ }
+}
diff --git a/Bukkit/src/main/java/com/github/intellectualsites/plotsquared/json/JSONString.java b/Bukkit/src/main/java/com/github/intellectualsites/plotsquared/json/JSONString.java
new file mode 100644
index 000000000..019776e58
--- /dev/null
+++ b/Bukkit/src/main/java/com/github/intellectualsites/plotsquared/json/JSONString.java
@@ -0,0 +1,16 @@
+package com.github.intellectualsites.plotsquared.json;
+
+/**
+ * The JSONString
interface allows a toJSONString()
method so that a class can change the
+ * behavior of JSONObject.toString()
, JSONArray.toString()
, and
+ * JSONWriter.value(
Object)
. The toJSONString
method will be used instead of the
+ * default behavior of using the Object's toString()
method and quoting the result.
+ */
+public interface JSONString {
+ /**
+ * The toJSONString
method allows a class to produce its own JSON serialization.
+ *
+ * @return A strictly syntactically correct JSON text.
+ */
+ String toJSONString();
+}
diff --git a/Bukkit/src/main/java/com/github/intellectualsites/plotsquared/json/JSONTokener.java b/Bukkit/src/main/java/com/github/intellectualsites/plotsquared/json/JSONTokener.java
new file mode 100644
index 000000000..290b9ea0b
--- /dev/null
+++ b/Bukkit/src/main/java/com/github/intellectualsites/plotsquared/json/JSONTokener.java
@@ -0,0 +1,384 @@
+package com.github.intellectualsites.plotsquared.json;
+
+import java.io.*;
+
+/**
+ * A JSONTokener takes a source string and extracts characters and tokens from it. It is used by the JSONObject and
+ * JSONArray constructors to parse JSON source strings.
+ *
+ * @author JSON.org
+ * @version 2014-05-03
+ */
+public class JSONTokener {
+ private final Reader reader;
+ private long character;
+ private boolean eof;
+ private long index;
+ private long line;
+ private char previous;
+ private boolean usePrevious;
+
+ /**
+ * Construct a JSONTokener from a Reader.
+ *
+ * @param reader A reader.
+ */
+ public JSONTokener(final Reader reader) {
+ this.reader = reader.markSupported() ? reader : new BufferedReader(reader);
+ eof = false;
+ usePrevious = false;
+ previous = 0;
+ index = 0;
+ character = 1;
+ line = 1;
+ }
+
+ /**
+ * Construct a JSONTokener from an InputStream.
+ *
+ * @param inputStream The source.
+ */
+ public JSONTokener(final InputStream inputStream) throws JSONException {
+ this(new InputStreamReader(inputStream));
+ }
+
+ /**
+ * Construct a JSONTokener from a string.
+ *
+ * @param s A source string.
+ */
+ public JSONTokener(final String s) {
+ this(new StringReader(s));
+ }
+
+ /**
+ * Get the hex value of a character (base16).
+ *
+ * @param c A character between '0' and '9' or between 'A' and 'F' or between 'a' and 'f'.
+ * @return An int between 0 and 15, or -1 if c was not a hex digit.
+ */
+ public static int dehexchar(final char c) {
+ if ((c >= '0') && (c <= '9')) {
+ return c - '0';
+ }
+ if ((c >= 'A') && (c <= 'F')) {
+ return c - ('A' - 10);
+ }
+ if ((c >= 'a') && (c <= 'f')) {
+ return c - ('a' - 10);
+ }
+ return -1;
+ }
+
+ /**
+ * Back up one character. This provides a sort of lookahead capability, so that you can test for a digit or letter
+ * before attempting to parse the next number or identifier.
+ */
+ public void back() throws JSONException {
+ if (usePrevious || (index <= 0)) {
+ throw new JSONException("Stepping back two steps is not supported");
+ }
+ index -= 1;
+ character -= 1;
+ usePrevious = true;
+ eof = false;
+ }
+
+ public boolean end() {
+ return eof && !usePrevious;
+ }
+
+ /**
+ * Determine if the source string still contains characters that next() can consume.
+ *
+ * @return true if not yet at the end of the source.
+ */
+ public boolean more() throws JSONException {
+ this.next();
+ if (end()) {
+ return false;
+ }
+ back();
+ return true;
+ }
+
+ /**
+ * Get the next character in the source string.
+ *
+ * @return The next character, or 0 if past the end of the source string.
+ */
+ public char next() throws JSONException {
+ int c;
+ if (usePrevious) {
+ usePrevious = false;
+ c = previous;
+ } else {
+ try {
+ c = reader.read();
+ } catch (final IOException exception) {
+ throw new JSONException(exception);
+ }
+ if (c <= 0) { // End of stream
+ eof = true;
+ c = 0;
+ }
+ }
+ index += 1;
+ if (previous == '\r') {
+ line += 1;
+ character = c == '\n' ? 0 : 1;
+ } else if (c == '\n') {
+ line += 1;
+ character = 0;
+ } else {
+ character += 1;
+ }
+ previous = (char) c;
+ return previous;
+ }
+
+ /**
+ * Consume the next character, and check that it matches a specified character.
+ *
+ * @param c The character to match.
+ * @return The character.
+ * @throws JSONException if the character does not match.
+ */
+ public char next(final char c) throws JSONException {
+ final char n = this.next();
+ if (n != c) {
+ throw syntaxError("Expected '" + c + "' and instead saw '" + n + "'");
+ }
+ return n;
+ }
+
+ /**
+ * Get the next n characters.
+ *
+ * @param n The number of characters to take.
+ * @return A string of n characters.
+ * @throws JSONException Substring bounds error if there are not n characters remaining in the source string.
+ */
+ public String next(final int n) throws JSONException {
+ if (n == 0) {
+ return "";
+ }
+ final char[] chars = new char[n];
+ int pos = 0;
+ while (pos < n) {
+ chars[pos] = this.next();
+ if (end()) {
+ throw syntaxError("Substring bounds error");
+ }
+ pos += 1;
+ }
+ return new String(chars);
+ }
+
+ /**
+ * Get the next char in the string, skipping whitespace.
+ *
+ * @return A character, or 0 if there are no more characters.
+ * @throws JSONException
+ */
+ public char nextClean() throws JSONException {
+ for (; ; ) {
+ final char c = this.next();
+ if ((c == 0) || (c > ' ')) {
+ return c;
+ }
+ }
+ }
+
+ /**
+ * Return the characters up to the next close quote character. Backslash processing is done. The formal JSON format
+ * does not allow strings in single quotes, but an implementation is allowed to accept them.
+ *
+ * @param quote The quoting character, either "
(double quote) or '
+ * (single quote) .
+ * @return A String.
+ * @throws JSONException Unterminated string.
+ */
+ public String nextString(final char quote) throws JSONException {
+ char c;
+ final StringBuilder sb = new StringBuilder();
+ for (; ; ) {
+ c = this.next();
+ switch (c) {
+ case 0:
+ case '\n':
+ case '\r':
+ throw syntaxError("Unterminated string");
+ case '\\':
+ c = this.next();
+ switch (c) {
+ case 'b':
+ sb.append('\b');
+ break;
+ case 't':
+ sb.append('\t');
+ break;
+ case 'n':
+ sb.append('\n');
+ break;
+ case 'f':
+ sb.append('\f');
+ break;
+ case 'r':
+ sb.append('\r');
+ break;
+ case 'u':
+ sb.append((char) Integer.parseInt(this.next(4), 16));
+ break;
+ case '"':
+ case '\'':
+ case '\\':
+ case '/':
+ sb.append(c);
+ break;
+ default:
+ throw syntaxError("Illegal escape.");
+ }
+ break;
+ default:
+ if (c == quote) {
+ return sb.toString();
+ }
+ sb.append(c);
+ }
+ }
+ }
+
+ /**
+ * Get the text up but not including the specified character or the end of line, whichever comes first.
+ *
+ * @param delimiter A delimiter character.
+ * @return A string.
+ */
+ public String nextTo(final char delimiter) throws JSONException {
+ final StringBuilder sb = new StringBuilder();
+ for (; ; ) {
+ final char c = this.next();
+ if ((c == delimiter) || (c == 0) || (c == '\n') || (c == '\r')) {
+ if (c != 0) {
+ back();
+ }
+ return sb.toString().trim();
+ }
+ sb.append(c);
+ }
+ }
+
+ /**
+ * Get the text up but not including one of the specified delimiter characters or the end of line, whichever comes
+ * first.
+ *
+ * @param delimiters A set of delimiter characters.
+ * @return A string, trimmed.
+ */
+ public String nextTo(final String delimiters) throws JSONException {
+ char c;
+ final StringBuilder sb = new StringBuilder();
+ for (; ; ) {
+ c = this.next();
+ if ((delimiters.indexOf(c) >= 0) || (c == 0) || (c == '\n') || (c == '\r')) {
+ if (c != 0) {
+ back();
+ }
+ return sb.toString().trim();
+ }
+ sb.append(c);
+ }
+ }
+
+ /**
+ * Get the next value. The value can be a Boolean, Double, Integer, JSONArray, JSONObject, Long, or String, or the
+ * JSONObject.NULL object.
+ *
+ * @return An object.
+ * @throws JSONException If syntax error.
+ */
+ public Object nextValue() throws JSONException {
+ char c = nextClean();
+ String string;
+ switch (c) {
+ case '"':
+ case '\'':
+ return nextString(c);
+ case '{':
+ back();
+ return new JSONObject(this);
+ case '[':
+ back();
+ return new JSONArray(this);
+ }
+ /*
+ * Handle unquoted text. This could be the values true, false, or
+ * null, or it can be a number. An implementation (such as this one)
+ * is allowed to also accept non-standard forms.
+ * Accumulate characters until we reach the end of the text or a
+ * formatting character.
+ */
+ final StringBuilder sb = new StringBuilder();
+ while ((c >= ' ') && (",:]}/\\\"[{;=#".indexOf(c) < 0)) {
+ sb.append(c);
+ c = this.next();
+ }
+ back();
+ string = sb.toString().trim();
+ if (string.isEmpty()) {
+ throw syntaxError("Missing value");
+ }
+ return JSONObject.stringToValue(string);
+ }
+
+ /**
+ * Skip characters until the next character is the requested character. If the requested character is not found, no
+ * characters are skipped.
+ *
+ * @param to A character to skip to.
+ * @return The requested character, or zero if the requested character is not found.
+ */
+ public char skipTo(final char to) throws JSONException {
+ char c;
+ try {
+ final long startIndex = index;
+ final long startCharacter = character;
+ final long startLine = line;
+ reader.mark(1000000);
+ do {
+ c = this.next();
+ if (c == 0) {
+ reader.reset();
+ index = startIndex;
+ character = startCharacter;
+ line = startLine;
+ return 0;
+ }
+ } while (c != to);
+ } catch (final IOException exception) {
+ throw new JSONException(exception);
+ }
+ back();
+ return c;
+ }
+
+ /**
+ * Make a JSONException to signal a syntax error.
+ *
+ * @param message The error message.
+ * @return A JSONException object, suitable for throwing
+ */
+ public JSONException syntaxError(final String message) {
+ return new JSONException(message + toString());
+ }
+
+ /**
+ * Make a printable string of this JSONTokener.
+ *
+ * @return " at {index} [character {character} line {line}]"
+ */
+ @Override public String toString() {
+ return " at " + index + " [character " + character + " line " + line + "]";
+ }
+}
diff --git a/Bukkit/src/main/java/com/github/intellectualsites/plotsquared/json/Property.java b/Bukkit/src/main/java/com/github/intellectualsites/plotsquared/json/Property.java
new file mode 100644
index 000000000..7ace1989b
--- /dev/null
+++ b/Bukkit/src/main/java/com/github/intellectualsites/plotsquared/json/Property.java
@@ -0,0 +1,52 @@
+package com.github.intellectualsites.plotsquared.json;
+
+import java.util.Enumeration;
+import java.util.Iterator;
+import java.util.Properties;
+
+/**
+ * Converts a Property file data into JSONObject and back.
+ *
+ * @author JSON.org
+ * @version 2014-05-03
+ */
+public class Property {
+ /**
+ * Converts a property file object into a JSONObject. The property file object is a table of name value pairs.
+ *
+ * @param properties java.util.Properties
+ * @return JSONObject
+ * @throws JSONException
+ */
+ public static JSONObject toJSONObject(final java.util.Properties properties)
+ throws JSONException {
+ final JSONObject jo = new JSONObject();
+ if ((properties != null) && !properties.isEmpty()) {
+ final Enumeration enumProperties = properties.propertyNames();
+ while (enumProperties.hasMoreElements()) {
+ final String name = (String) enumProperties.nextElement();
+ jo.put(name, properties.getProperty(name));
+ }
+ }
+ return jo;
+ }
+
+ /**
+ * Converts the JSONObject into a property file object.
+ *
+ * @param jo JSONObject
+ * @return java.util.Properties
+ * @throws JSONException
+ */
+ public static Properties toProperties(final JSONObject jo) throws JSONException {
+ final Properties properties = new Properties();
+ if (jo != null) {
+ final Iterator keys = jo.keys();
+ while (keys.hasNext()) {
+ final String name = keys.next();
+ properties.put(name, jo.getString(name));
+ }
+ }
+ return properties;
+ }
+}
diff --git a/Bukkit/src/main/java/com/github/intellectualsites/plotsquared/json/XML.java b/Bukkit/src/main/java/com/github/intellectualsites/plotsquared/json/XML.java
new file mode 100644
index 000000000..51a0cdb10
--- /dev/null
+++ b/Bukkit/src/main/java/com/github/intellectualsites/plotsquared/json/XML.java
@@ -0,0 +1,375 @@
+package com.github.intellectualsites.plotsquared.json;
+
+import java.util.Iterator;
+
+/**
+ * This provides static methods to convert an XML text into a JSONObject, and to covert a JSONObject into an XML text.
+ *
+ * @author JSON.org
+ * @version 2014-05-03
+ */
+class XML {
+
+ static final Character AMP = '&';
+ static final Character APOS = '\'';
+ static final Character BANG = '!';
+ static final Character EQ = '=';
+ static final Character GT = '>';
+ static final Character LT = '<';
+ static final Character QUEST = '?';
+ static final Character QUOT = '"';
+ static final Character SLASH = '/';
+
+ static String escape(String string) {
+ StringBuilder sb = new StringBuilder(string.length());
+ for (int i = 0, length = string.length(); i < length; i++) {
+ char c = string.charAt(i);
+ switch (c) {
+ case '&':
+ sb.append("&");
+ break;
+ case '<':
+ sb.append("<");
+ break;
+ case '>':
+ sb.append(">");
+ break;
+ case '"':
+ sb.append(""");
+ break;
+ case '\'':
+ sb.append("'");
+ break;
+ default:
+ sb.append(c);
+ }
+ }
+ return sb.toString();
+ }
+
+ /**
+ * Throw an exception if the string contains whitespace. Whitespace is not allowed in tagNames and attributes.
+ *
+ * @param string A string.
+ * @throws JSONException
+ */
+ static void noSpace(String string) throws JSONException {
+ int length = string.length();
+ if (length == 0) {
+ throw new JSONException("Empty string.");
+ }
+ for (char c : string.toCharArray()) {
+ if (Character.isWhitespace(c)) {
+ throw new JSONException('\'' + string + "' contains a space character.");
+ }
+ }
+ }
+
+ /**
+ * Scan the content following the named tag, attaching it to the context.
+ *
+ * @param x The XMLTokener containing the source string.
+ * @param context The JSONObject that will include the new material.
+ * @param name The tag name.
+ * @return true if the close tag is processed.
+ * @throws JSONException
+ */
+ private static boolean parse(XMLTokener x, JSONObject context, String name)
+ throws JSONException {
+ // Test for and skip past these forms:
+ //
+ //
+ //
+ // ... ?>
+ // Report errors for these forms:
+ // <>
+ // <=
+ // <<
+ Object token = x.nextToken();
+ // ");
+ return false;
+ }
+ x.back();
+ } else if (c == '[') {
+ token = x.nextToken();
+ if ("CDATA".equals(token)) {
+ if (x.next() == '[') {
+ string = x.nextCDATA();
+ if (!string.isEmpty()) {
+ context.accumulate("content", string);
+ }
+ return false;
+ }
+ }
+ throw x.syntaxError("Expected 'CDATA['");
+ }
+ int i = 1;
+ do {
+ token = x.nextMeta();
+ if (token == null) {
+ throw x.syntaxError("Missing '>' after ' 0);
+ return false;
+ } else if (token == QUEST) {
+ //
+ x.skipPast("?>");
+ return false;
+ } else if (token == SLASH) {
+ // Close tag
+ token = x.nextToken();
+ if (name == null) {
+ throw x.syntaxError("Mismatched close tag " + token);
+ }
+ if (!token.equals(name)) {
+ throw x.syntaxError("Mismatched " + name + " and " + token);
+ }
+ if (x.nextToken() != GT) {
+ throw x.syntaxError("Misshaped close tag");
+ }
+ return true;
+ } else if (token instanceof Character) {
+ throw x.syntaxError("Misshaped tag");
+ // Open tag <
+ } else {
+ String tagName = (String) token;
+ token = null;
+ JSONObject jsonobject = new JSONObject();
+ for (; ; ) {
+ if (token == null) {
+ token = x.nextToken();
+ }
+ // attribute = value
+ if (token instanceof String) {
+ string = (String) token;
+ token = x.nextToken();
+ if (token == EQ) {
+ token = x.nextToken();
+ if (!(token instanceof String)) {
+ throw x.syntaxError("Missing value");
+ }
+ jsonobject.accumulate(string, XML.stringToValue((String) token));
+ token = null;
+ } else {
+ jsonobject.accumulate(string, "");
+ }
+ // Empty tag <.../>
+ } else if (token == SLASH) {
+ if (x.nextToken() != GT) {
+ throw x.syntaxError("Misshaped tag");
+ }
+ if (jsonobject.length() > 0) {
+ context.accumulate(tagName, jsonobject);
+ } else {
+ context.accumulate(tagName, "");
+ }
+ return false;
+ // Content, between <...> and
+ } else if (token == GT) {
+ for (; ; ) {
+ token = x.nextContent();
+ if (token == null) {
+ if (tagName != null) {
+ throw x.syntaxError("Unclosed tag " + tagName);
+ }
+ return false;
+ } else if (token instanceof String) {
+ string = (String) token;
+ if (!string.isEmpty()) {
+ jsonobject.accumulate("content", XML.stringToValue(string));
+ }
+ // Nested element
+ } else if (token == LT) {
+ if (parse(x, jsonobject, tagName)) {
+ if (jsonobject.length() == 0) {
+ context.accumulate(tagName, "");
+ } else if ((jsonobject.length() == 1) && (jsonobject.opt("content")
+ != null)) {
+ context.accumulate(tagName, jsonobject.opt("content"));
+ } else {
+ context.accumulate(tagName, jsonobject);
+ }
+ return false;
+ }
+ }
+ }
+ } else {
+ throw x.syntaxError("Misshaped tag");
+ }
+ }
+ }
+ }
+
+ /**
+ * Try to convert a string into a number, boolean, or null. If the string can't be converted, return the string.
+ * This is much less ambitious than JSONObject.stringToValue, especially because it does not attempt to convert plus
+ * forms, octal forms, hex forms, or E forms lacking decimal points.
+ *
+ * @param string A String.
+ * @return A simple JSON value.
+ */
+ static Object stringToValue(String string) {
+ if ("true".equalsIgnoreCase(string)) {
+ return Boolean.TRUE;
+ }
+ if ("false".equalsIgnoreCase(string)) {
+ return Boolean.FALSE;
+ }
+ if ("null".equalsIgnoreCase(string)) {
+ return JSONObject.NULL;
+ }
+ //If it might be a number, try converting it, first as a Long, and then as a Double. If that doesn't work, return the string.
+ try {
+ char initial = string.charAt(0);
+ if ((initial == '-') || ((initial >= '0') && (initial <= '9'))) {
+ Long value = Long.valueOf(string);
+ if (value.toString().equals(string)) {
+ return value;
+ }
+ }
+ } catch (NumberFormatException ignore) {
+ try {
+ Double value = Double.valueOf(string);
+ if (value.toString().equals(string)) {
+ return value;
+ }
+ } catch (NumberFormatException ignored) {
+ }
+ }
+ return string;
+ }
+
+ public static JSONObject toJSONObject(String string) throws JSONException {
+ JSONObject jo = new JSONObject();
+ XMLTokener x = new XMLTokener(string);
+ while (x.more() && x.skipPast("<")) {
+ parse(x, jo, null);
+ }
+ return jo;
+ }
+
+ /**
+ * Convert a JSONObject into a well-formed, element-normal XML string.
+ *
+ * @param object A JSONObject.
+ * @return A string.
+ * @throws JSONException
+ */
+ public static String toString(Object object) throws JSONException {
+ return toString(object, null);
+ }
+
+ /**
+ * Convert a JSONObject into a well-formed, element-normal XML string.
+ *
+ * @param object A JSONObject.
+ * @param tagName The optional name of the enclosing tag.
+ * @return A string.
+ * @throws JSONException
+ */
+ public static String toString(Object object, String tagName) throws JSONException {
+ StringBuilder sb = new StringBuilder();
+ int i;
+ JSONArray ja;
+ int length;
+ String string;
+ if (object instanceof JSONObject) {
+ // Emit
+ if (tagName != null) {
+ sb.append('<');
+ sb.append(tagName);
+ sb.append('>');
+ }
+ // Loop thru the keys.
+ JSONObject jo = (JSONObject) object;
+ Iterator keys = jo.keys();
+ while (keys.hasNext()) {
+ String key = keys.next();
+ Object value = jo.opt(key);
+ if (value == null) {
+ value = "";
+ }
+ string = value instanceof String ? (String) value : null;
+ // Emit content in body
+ if ("content".equals(key)) {
+ if (value instanceof JSONArray) {
+ ja = (JSONArray) value;
+ length = ja.length();
+ for (i = 0; i < length; i += 1) {
+ if (i > 0) {
+ sb.append('\n');
+ }
+ sb.append(escape(ja.get(i).toString()));
+ }
+ } else {
+ sb.append(escape(value.toString()));
+ }
+ // Emit an array of similar keys
+ } else if (value instanceof JSONArray) {
+ ja = (JSONArray) value;
+ length = ja.length();
+ for (i = 0; i < length; i += 1) {
+ value = ja.get(i);
+ if (value instanceof JSONArray) {
+ sb.append('<');
+ sb.append(key);
+ sb.append('>');
+ sb.append(toString(value));
+ sb.append("");
+ sb.append(key);
+ sb.append('>');
+ } else {
+ sb.append(toString(value, key));
+ }
+ }
+ } else if ("".equals(value)) {
+ sb.append('<');
+ sb.append(key);
+ sb.append("/>");
+ // Emit a new tag
+ } else {
+ sb.append(toString(value, key));
+ }
+ }
+ if (tagName != null) {
+ // Emit the close tag
+ sb.append("");
+ sb.append(tagName);
+ sb.append('>');
+ }
+ return sb.toString();
+ // XML does not have good support for arrays. If an array appears in
+ // a place
+ // where XML is lacking, synthesize an element.
+ } else {
+ if (object.getClass().isArray()) {
+ object = new JSONArray(object);
+ }
+ if (object instanceof JSONArray) {
+ ja = (JSONArray) object;
+ length = ja.length();
+ for (i = 0; i < length; i += 1) {
+ sb.append(toString(ja.opt(i), tagName == null ? "array" : tagName));
+ }
+ return sb.toString();
+ } else {
+ string = escape(object.toString());
+ return (tagName == null) ?
+ '"' + string + '"' :
+ string.isEmpty() ?
+ '<' + tagName + "/>" :
+ '<' + tagName + '>' + string + "" + tagName + '>';
+ }
+ }
+ }
+}
diff --git a/Bukkit/src/main/java/com/github/intellectualsites/plotsquared/json/XMLTokener.java b/Bukkit/src/main/java/com/github/intellectualsites/plotsquared/json/XMLTokener.java
new file mode 100644
index 000000000..96d65541f
--- /dev/null
+++ b/Bukkit/src/main/java/com/github/intellectualsites/plotsquared/json/XMLTokener.java
@@ -0,0 +1,315 @@
+package com.github.intellectualsites.plotsquared.json;
+
+import java.util.HashMap;
+
+/**
+ * The XMLTokener extends the JSONTokener to provide additional methods for the parsing of XML texts.
+ *
+ * @author JSON.org
+ * @version 2014-05-03
+ */
+public class XMLTokener extends JSONTokener {
+ /**
+ * The table of entity values. It initially contains Character values for amp, apos, gt, lt, quot.
+ */
+ public static final HashMap entity;
+
+ static {
+ entity = new HashMap<>(8);
+ entity.put("amp", XML.AMP);
+ entity.put("apos", XML.APOS);
+ entity.put("gt", XML.GT);
+ entity.put("lt", XML.LT);
+ entity.put("quot", XML.QUOT);
+ }
+
+ /**
+ * Construct an XMLTokener from a string.
+ *
+ * @param s A source string.
+ */
+ public XMLTokener(final String s) {
+ super(s);
+ }
+
+ /**
+ * Get the text in the CDATA block.
+ *
+ * @return The string up to the ]]>
.
+ * @throws JSONException If the ]]>
is not found.
+ */
+ public String nextCDATA() throws JSONException {
+ final StringBuilder sb = new StringBuilder();
+ for (; ; ) {
+ char c = next();
+ if (end()) {
+ throw syntaxError("Unclosed CDATA");
+ }
+ sb.append(c);
+ int i = sb.length() - 3;
+ if ((i >= 0) && (sb.charAt(i) == ']') && (sb.charAt(i + 1) == ']') && (sb.charAt(i + 2)
+ == '>')) {
+ sb.setLength(i);
+ return sb.toString();
+ }
+ }
+ }
+
+ /**
+ * Get the next XML outer token, trimming whitespace. There are two kinds of tokens: the '<' character which begins
+ * a markup tag, and the content text between markup tags.
+ *
+ * @return A string, or a '<' Character, or null if there is no more source text.
+ * @throws JSONException
+ */
+ public Object nextContent() throws JSONException {
+ char c;
+ do {
+ c = next();
+ } while (Character.isWhitespace(c));
+ if (c == 0) {
+ return null;
+ }
+ if (c == '<') {
+ return XML.LT;
+ }
+ StringBuilder sb = new StringBuilder();
+ for (; ; ) {
+ if ((c == '<') || (c == 0)) {
+ back();
+ return sb.toString().trim();
+ }
+ if (c == '&') {
+ sb.append(nextEntity('&'));
+ } else {
+ sb.append(c);
+ }
+ c = next();
+ }
+ }
+
+ /**
+ * Return the next entity. These entities are translated to Characters: & " > <
+ * "
.
+ *
+ * @param ampersand An ampersand character.
+ * @return A Character or an entity String if the entity is not recognized.
+ * @throws JSONException If missing ';' in XML entity.
+ */
+ public Object nextEntity(final char ampersand) throws JSONException {
+ final StringBuilder sb = new StringBuilder();
+ for (; ; ) {
+ final char c = next();
+ if (Character.isLetterOrDigit(c) || (c == '#')) {
+ sb.append(Character.toLowerCase(c));
+ } else if (c == ';') {
+ break;
+ } else {
+ throw syntaxError("Missing ';' in XML entity: &" + sb);
+ }
+ }
+ final String string = sb.toString();
+ final Object object = entity.get(string);
+ return object != null ? object : ampersand + string + ';';
+ }
+
+ /**
+ * Returns the next XML meta token. This is used for skipping over and ...?> structures.
+ *
+ * @return Syntax characters (< > / = ! ?
) are returned as Character, and strings and names are
+ * returned as Boolean. We don't care what the values actually are.
+ * @throws JSONException If a string is not properly closed or if the XML is badly structured.
+ */
+ public Object nextMeta() throws JSONException {
+ char c;
+ do {
+ c = next();
+ } while (Character.isWhitespace(c));
+ char q;
+ switch (c) {
+ case 0:
+ throw syntaxError("Misshaped meta tag");
+ case '<':
+ return XML.LT;
+ case '>':
+ return XML.GT;
+ case '/':
+ return XML.SLASH;
+ case '=':
+ return XML.EQ;
+ case '!':
+ return XML.BANG;
+ case '?':
+ return XML.QUEST;
+ case '"':
+ case '\'':
+ q = c;
+ for (; ; ) {
+ c = next();
+ if (c == 0) {
+ throw syntaxError("Unterminated string");
+ }
+ if (c == q) {
+ return Boolean.TRUE;
+ }
+ }
+ default:
+ for (; ; ) {
+ c = next();
+ if (Character.isWhitespace(c)) {
+ return Boolean.TRUE;
+ }
+ switch (c) {
+ case 0:
+ case '<':
+ case '>':
+ case '/':
+ case '=':
+ case '!':
+ case '?':
+ case '"':
+ case '\'':
+ back();
+ return Boolean.TRUE;
+ }
+ }
+ }
+ }
+
+ /**
+ * Get the next XML Token. These tokens are found inside of angle brackets. It may be one of these characters:
+ * / >= ! ?
or it may be a string wrapped in single quotes or double quotes, or it may be a name.
+ *
+ * @return a String or a Character.
+ * @throws JSONException If the XML is not well formed.
+ */
+ public Object nextToken() throws JSONException {
+ char c;
+ do {
+ c = next();
+ } while (Character.isWhitespace(c));
+ char q;
+ StringBuilder sb;
+ switch (c) {
+ case 0:
+ throw syntaxError("Misshaped element");
+ case '<':
+ throw syntaxError("Misplaced '<'");
+ case '>':
+ return XML.GT;
+ case '/':
+ return XML.SLASH;
+ case '=':
+ return XML.EQ;
+ case '!':
+ return XML.BANG;
+ case '?':
+ return XML.QUEST;
+ // Quoted string
+ case '"':
+ case '\'':
+ q = c;
+ sb = new StringBuilder();
+ for (; ; ) {
+ c = next();
+ if (c == 0) {
+ throw syntaxError("Unterminated string");
+ }
+ if (c == q) {
+ return sb.toString();
+ }
+ if (c == '&') {
+ sb.append(nextEntity('&'));
+ } else {
+ sb.append(c);
+ }
+ }
+ default:
+ // Name
+ sb = new StringBuilder();
+ for (; ; ) {
+ sb.append(c);
+ c = next();
+ if (Character.isWhitespace(c)) {
+ return sb.toString();
+ }
+ switch (c) {
+ case 0:
+ return sb.toString();
+ case '>':
+ case '/':
+ case '=':
+ case '!':
+ case '?':
+ case '[':
+ case ']':
+ back();
+ return sb.toString();
+ case '<':
+ case '"':
+ case '\'':
+ throw syntaxError("Bad character in a name");
+ }
+ }
+ }
+ }
+
+ /**
+ * Skip characters until past the requested string. If it is not found, we are left at the end of the source with a
+ * result of false.
+ *
+ * @param to A string to skip past.
+ * @throws JSONException
+ */
+ public boolean skipPast(final String to) throws JSONException {
+ char c;
+ int i;
+ final int length = to.length();
+ final char[] circle = new char[length];
+ /*
+ * First fill the circle buffer with as many characters as are in the
+ * to string. If we reach an early end, bail.
+ */
+ for (i = 0; i < length; i += 1) {
+ c = next();
+ if (c == 0) {
+ return false;
+ }
+ circle[i] = c;
+ }
+ /* We will loop, possibly for all of the remaining characters. */
+ for (int offset = 0; ; ) {
+ int j = offset;
+ boolean b = true;
+ /* Compare the circle buffer with the to string. */
+ for (i = 0; i < length; i += 1) {
+ if (circle[j] != to.charAt(i)) {
+ b = false;
+ break;
+ }
+ j += 1;
+ if (j >= length) {
+ j -= length;
+ }
+ }
+ /* If we exit the loop with b intact, then victory is ours. */
+ if (b) {
+ return true;
+ }
+ /* Get the next character. If there isn't one, then defeat is ours. */
+ c = next();
+ if (c == 0) {
+ return false;
+ }
+ /*
+ * Shove the character in the circle buffer and advance the
+ * circle offset. The offset is mod n.
+ */
+ circle[offset] = c;
+ offset += 1;
+ if (offset >= length) {
+ offset -= length;
+ }
+ }
+ }
+}
diff --git a/Bukkit/src/main/java/com/github/intellectualsites/plotsquared/plot/IPlotMain.java b/Bukkit/src/main/java/com/github/intellectualsites/plotsquared/plot/IPlotMain.java
new file mode 100644
index 000000000..2f6559a4e
--- /dev/null
+++ b/Bukkit/src/main/java/com/github/intellectualsites/plotsquared/plot/IPlotMain.java
@@ -0,0 +1,285 @@
+package com.github.intellectualsites.plotsquared.plot;
+
+import com.github.intellectualsites.plotsquared.plot.generator.GeneratorWrapper;
+import com.github.intellectualsites.plotsquared.plot.generator.HybridUtils;
+import com.github.intellectualsites.plotsquared.plot.generator.IndependentPlotGenerator;
+import com.github.intellectualsites.plotsquared.plot.logger.ILogger;
+import com.github.intellectualsites.plotsquared.plot.object.BlockRegistry;
+import com.github.intellectualsites.plotsquared.plot.object.PlotPlayer;
+import com.github.intellectualsites.plotsquared.plot.util.*;
+import com.github.intellectualsites.plotsquared.plot.util.block.QueueProvider;
+
+import java.io.File;
+import java.util.List;
+
+public interface IPlotMain extends ILogger {
+
+ /**
+ * Log a message to console.
+ *
+ * @param message The message to log
+ */
+ void log(String message);
+
+ /**
+ * Get the `PlotSquared` directory.
+ *
+ * @return The plugin directory
+ */
+ File getDirectory();
+
+ /**
+ * Get the directory containing all the worlds.
+ *
+ * @return The directory containing the worlds
+ */
+ File getWorldContainer();
+
+ /**
+ * Wrap a player into a PlotPlayer object.
+ *
+ * @param player The player to convert to a PlotPlayer
+ * @return A PlotPlayer
+ */
+ PlotPlayer wrapPlayer(Object player);
+
+ /**
+ * Disable the implementation.
+ *
+ *
+ * If a full disable isn't feasibly, just disable what it can.
+ *
+ */
+ void disable();
+
+ /**
+ * Completely shut down the plugin
+ */
+ void shutdown();
+
+ /**
+ * Get the version of the PlotSquared being used.
+ *
+ * @return the plugin version
+ */
+ int[] getPluginVersion();
+
+ /**
+ * Get the version of the PlotSquared being used as a string.
+ *
+ * @return the plugin version as a string
+ */
+ String getPluginVersionString();
+
+ /**
+ * Usually PlotSquared
+ *
+ * @return
+ */
+ default String getPluginName() {
+ return "PlotSquared";
+ }
+
+ /**
+ * Get the version of Minecraft that is running.
+ *
+ * @return
+ */
+ int[] getServerVersion();
+
+ /**
+ * Get the server implementation name and version
+ */
+ String getServerImplementation();
+
+ /**
+ * Get the NMS package prefix.
+ *
+ * @return The NMS package prefix
+ */
+ String getNMSPackage();
+
+ /**
+ * Get the schematic handler.
+ *
+ * @return The {@link SchematicHandler}
+ */
+ SchematicHandler initSchematicHandler();
+
+ /**
+ * Get the Chat Manager.
+ *
+ * @return The {@link ChatManager}
+ */
+ ChatManager initChatManager();
+
+ /**
+ * The task manager will run and manage Minecraft tasks.
+ *
+ * @return
+ */
+ TaskManager getTaskManager();
+
+ /**
+ * Run the task that will kill road mobs.
+ */
+ void runEntityTask();
+
+ /**
+ * Register the implementation specific commands.
+ */
+ void registerCommands();
+
+ /**
+ * Register the protection system.
+ */
+ void registerPlayerEvents();
+
+ /**
+ * Register inventory related events.
+ */
+ void registerInventoryEvents();
+
+ /**
+ * Register plot plus related events.
+ */
+ void registerPlotPlusEvents();
+
+ /**
+ * Register force field events.
+ */
+ void registerForceFieldEvents();
+
+ /**
+ * Register the WorldEdit hook.
+ */
+ boolean initWorldEdit();
+
+ /**
+ * Get the economy provider.
+ *
+ * @return
+ */
+ EconHandler getEconomyHandler();
+
+ /**
+ * Get the {@link QueueProvider} class.
+ *
+ * @return
+ */
+ QueueProvider initBlockQueue();
+
+ /**
+ * Get the {@link WorldUtil} class.
+ *
+ * @return
+ */
+ WorldUtil initWorldUtil();
+
+ /**
+ * Get the EventUtil class.
+ *
+ * @return
+ */
+ EventUtil initEventUtil();
+
+ /**
+ * Get the chunk manager.
+ *
+ * @return
+ */
+ ChunkManager initChunkManager();
+
+ /**
+ * Get the {@link SetupUtils} class.
+ *
+ * @return
+ */
+ SetupUtils initSetupUtils();
+
+ /**
+ * Get {@link HybridUtils} class.
+ *
+ * @return
+ */
+ HybridUtils initHybridUtils();
+
+ /**
+ * Start Metrics.
+ */
+ void startMetrics();
+
+ /**
+ * If a world is already loaded, set the generator (use NMS if required).
+ *
+ * @param world The world to set the generator
+ */
+ void setGenerator(String world);
+
+ /**
+ * Get the {@link UUIDHandlerImplementation} which will cache and
+ * provide UUIDs.
+ *
+ * @return
+ */
+ UUIDHandlerImplementation initUUIDHandler();
+
+ /**
+ * Get the {@link InventoryUtil} class (used for implementation specific
+ * inventory guis).
+ *
+ * @return
+ */
+ InventoryUtil initInventoryUtil();
+
+ /**
+ * Unregister a PlotPlayer from cache e.g. if they have logged off.
+ *
+ * @param player
+ */
+ void unregister(PlotPlayer player);
+
+ /**
+ * Get the generator wrapper for a world (world) and generator (name).
+ *
+ * @param world
+ * @param name
+ * @return
+ */
+ GeneratorWrapper> getGenerator(String world, String name);
+
+ GeneratorWrapper> wrapPlotGenerator(String world, IndependentPlotGenerator generator);
+
+ /**
+ * Register the chunk processor which will clean out chunks that have too
+ * many blockstates or entities.
+ */
+ void registerChunkProcessor();
+
+ /**
+ * Register the world initialization events (used to keep track of worlds
+ * being generated).
+ */
+ void registerWorldEvents();
+
+ /**
+ * Usually HybridGen
+ *
+ * @return Default implementation generator
+ */
+ IndependentPlotGenerator getDefaultGenerator();
+
+ /**
+ * Get the class that will manage player titles.
+ *
+ * @return
+ */
+ AbstractTitle initTitleManager();
+
+ List getPluginIds();
+
+ BlockRegistry> getBlockRegistry();
+
+ LegacyMappings getLegacyMappings();
+
+}
diff --git a/Bukkit/src/main/java/com/github/intellectualsites/plotsquared/plot/Platform.java b/Bukkit/src/main/java/com/github/intellectualsites/plotsquared/plot/Platform.java
new file mode 100644
index 000000000..9dfbf2ffb
--- /dev/null
+++ b/Bukkit/src/main/java/com/github/intellectualsites/plotsquared/plot/Platform.java
@@ -0,0 +1,6 @@
+package com.github.intellectualsites.plotsquared.plot;
+
+public enum Platform {
+ Bukkit, Sponge, Spigot
+
+}
diff --git a/Bukkit/src/main/java/com/github/intellectualsites/plotsquared/plot/PlotSquared.java b/Bukkit/src/main/java/com/github/intellectualsites/plotsquared/plot/PlotSquared.java
new file mode 100644
index 000000000..63bbf8142
--- /dev/null
+++ b/Bukkit/src/main/java/com/github/intellectualsites/plotsquared/plot/PlotSquared.java
@@ -0,0 +1,2066 @@
+package com.github.intellectualsites.plotsquared.plot;
+
+import com.github.intellectualsites.plotsquared.configuration.ConfigurationSection;
+import com.github.intellectualsites.plotsquared.configuration.MemorySection;
+import com.github.intellectualsites.plotsquared.configuration.file.YamlConfiguration;
+import com.github.intellectualsites.plotsquared.configuration.serialization.ConfigurationSerialization;
+import com.github.intellectualsites.plotsquared.plot.commands.WE_Anywhere;
+import com.github.intellectualsites.plotsquared.plot.config.C;
+import com.github.intellectualsites.plotsquared.plot.config.Configuration;
+import com.github.intellectualsites.plotsquared.plot.config.Settings;
+import com.github.intellectualsites.plotsquared.plot.config.Storage;
+import com.github.intellectualsites.plotsquared.plot.database.*;
+import com.github.intellectualsites.plotsquared.plot.generator.GeneratorWrapper;
+import com.github.intellectualsites.plotsquared.plot.generator.HybridPlotWorld;
+import com.github.intellectualsites.plotsquared.plot.generator.HybridUtils;
+import com.github.intellectualsites.plotsquared.plot.generator.IndependentPlotGenerator;
+import com.github.intellectualsites.plotsquared.plot.listener.WESubscriber;
+import com.github.intellectualsites.plotsquared.plot.logger.ILogger;
+import com.github.intellectualsites.plotsquared.plot.object.*;
+import com.github.intellectualsites.plotsquared.plot.object.worlds.DefaultPlotAreaManager;
+import com.github.intellectualsites.plotsquared.plot.object.worlds.PlotAreaManager;
+import com.github.intellectualsites.plotsquared.plot.object.worlds.SinglePlotArea;
+import com.github.intellectualsites.plotsquared.plot.object.worlds.SinglePlotAreaManager;
+import com.github.intellectualsites.plotsquared.plot.util.*;
+import com.github.intellectualsites.plotsquared.plot.util.block.GlobalBlockQueue;
+import com.github.intellectualsites.plotsquared.plot.util.expiry.ExpireManager;
+import com.github.intellectualsites.plotsquared.plot.util.expiry.ExpiryTask;
+import com.sk89q.worldedit.WorldEdit;
+import lombok.Getter;
+import lombok.NonNull;
+import lombok.Setter;
+
+import javax.annotation.Nullable;
+import java.io.*;
+import java.net.MalformedURLException;
+import java.net.URISyntaxException;
+import java.net.URL;
+import java.net.URLConnection;
+import java.nio.file.Files;
+import java.sql.SQLException;
+import java.util.*;
+import java.util.Map.Entry;
+import java.util.regex.Pattern;
+import java.util.zip.ZipEntry;
+import java.util.zip.ZipInputStream;
+
+/**
+ * An implementation of the core, with a static getter for easy access.
+ */
+@SuppressWarnings({"unused", "WeakerAccess"}) public class PlotSquared {
+
+ private static final Set EMPTY_SET = Collections.unmodifiableSet(Collections.emptySet());
+ private static PlotSquared instance;
+ // Implementation
+ public final IPlotMain IMP;
+ // Current thread
+ private final Thread thread;
+ // WorldEdit instance
+ public WorldEdit worldedit;
+ public File styleFile;
+ public File configFile;
+ public File worldsFile;
+ public File commandsFile;
+ public File translationFile;
+ public YamlConfiguration style;
+ public YamlConfiguration config;
+ public YamlConfiguration worlds;
+ public YamlConfiguration storage;
+ public YamlConfiguration commands;
+ // Temporary hold the plots/clusters before the worlds load
+ public HashMap> clusters_tmp;
+ public HashMap> plots_tmp;
+ // Implementation logger
+ @Setter @Getter private ILogger logger;
+ // Platform / Version / Update URL
+ private PlotVersion version;
+ // Files and configuration
+ @Getter private File jarFile = null; // This file
+ private File storageFile;
+ @Getter private PlotAreaManager plotAreaManager;
+
+ /**
+ * Initialize PlotSquared with the desired Implementation class.
+ *
+ * @param iPlotMain Implementation of {@link IPlotMain} used
+ * @param platform The platform being used
+ */
+ public PlotSquared(final IPlotMain iPlotMain, final String platform) {
+ if (instance != null) {
+ throw new IllegalStateException("Cannot re-initialize the PlotSquared singleton");
+ }
+ instance = this;
+
+ this.thread = Thread.currentThread();
+ this.IMP = iPlotMain;
+ this.logger = iPlotMain;
+ Settings.PLATFORM = platform;
+
+ //
+ // Register configuration serializable classes
+ //
+ ConfigurationSerialization.registerClass(PlotBlock.class, "PlotBlock");
+ ConfigurationSerialization.registerClass(BlockBucket.class, "BlockBucket");
+
+ try {
+ new ReflectionUtils(this.IMP.getNMSPackage());
+ try {
+ URL url = PlotSquared.class.getProtectionDomain().getCodeSource().getLocation();
+ this.jarFile = new File(
+ new URL(url.toURI().toString().split("\\!")[0].replaceAll("jar:file", "file"))
+ .toURI().getPath());
+ } catch (MalformedURLException | URISyntaxException | SecurityException e) {
+ e.printStackTrace();
+ this.jarFile = new File(this.IMP.getDirectory().getParentFile(), "PlotSquared.jar");
+ if (!this.jarFile.exists()) {
+ this.jarFile = new File(this.IMP.getDirectory().getParentFile(),
+ "PlotSquared-" + platform + ".jar");
+ }
+ }
+ TaskManager.IMP = this.IMP.getTaskManager();
+
+ // World Util. Has to be done before config files are loaded
+ WorldUtil.IMP = this.IMP.initWorldUtil();
+
+ if (!setupConfigs()) {
+ return;
+ }
+ this.translationFile = MainUtil.getFile(this.IMP.getDirectory(),
+ Settings.Paths.TRANSLATIONS + File.separator + IMP.getPluginName()
+ + ".use_THIS.yml");
+ C.load(this.translationFile);
+
+ // Setup plotAreaManager
+ if (Settings.Enabled_Components.WORLDS) {
+ this.plotAreaManager = new SinglePlotAreaManager();
+ } else {
+ this.plotAreaManager = new DefaultPlotAreaManager();
+ }
+
+ // Database
+ if (Settings.Enabled_Components.DATABASE) {
+ setupDatabase();
+ }
+ // Comments
+ CommentManager.registerDefaultInboxes();
+ // Kill entities
+ if (Settings.Enabled_Components.KILL_ROAD_MOBS
+ || Settings.Enabled_Components.KILL_ROAD_VEHICLES) {
+ this.IMP.runEntityTask();
+ }
+ if (Settings.Enabled_Components.EVENTS) {
+ this.IMP.registerPlayerEvents();
+ this.IMP.registerInventoryEvents();
+ this.IMP.registerPlotPlusEvents();
+ }
+ // Required
+ this.IMP.registerWorldEvents();
+ if (Settings.Enabled_Components.METRICS) {
+ this.IMP.startMetrics();
+ } else {
+ PlotSquared.log(C.CONSOLE_PLEASE_ENABLE_METRICS.f(IMP.getPluginName()));
+ }
+ if (Settings.Enabled_Components.CHUNK_PROCESSOR) {
+ this.IMP.registerChunkProcessor();
+ }
+ // create UUIDWrapper
+ UUIDHandler.implementation = this.IMP.initUUIDHandler();
+ if (Settings.Enabled_Components.UUID_CACHE) {
+ startUuidCatching();
+ } else {
+ // Start these separately
+ UUIDHandler.add(new StringWrapper("*"), DBFunc.EVERYONE);
+ startExpiryTasks();
+ }
+ // create event util class
+ EventUtil.manager = this.IMP.initEventUtil();
+ // create Hybrid utility class
+ HybridUtils.manager = this.IMP.initHybridUtils();
+ // Inventory utility class
+ InventoryUtil.manager = this.IMP.initInventoryUtil();
+ // create setup util class
+ SetupUtils.manager = this.IMP.initSetupUtils();
+ // Set block
+ GlobalBlockQueue.IMP = new GlobalBlockQueue(IMP.initBlockQueue(), 1);
+ GlobalBlockQueue.IMP.runTask();
+ // Set chunk
+ ChunkManager.manager = this.IMP.initChunkManager();
+ // Schematic handler
+ SchematicHandler.manager = this.IMP.initSchematicHandler();
+ // Titles
+ AbstractTitle.TITLE_CLASS = this.IMP.initTitleManager();
+ // Chat
+ ChatManager.manager = this.IMP.initChatManager();
+ // Commands
+ if (Settings.Enabled_Components.COMMANDS) {
+ this.IMP.registerCommands();
+ }
+ // WorldEdit
+ if (Settings.Enabled_Components.WORLDEDIT_RESTRICTIONS) {
+ try {
+ if (this.IMP.initWorldEdit()) {
+ PlotSquared.debug(IMP.getPluginName() + " hooked into WorldEdit.");
+ this.worldedit = WorldEdit.getInstance();
+ WorldEdit.getInstance().getEventBus().register(new WESubscriber());
+ if (Settings.Enabled_Components.COMMANDS) {
+ new WE_Anywhere();
+ }
+
+ }
+ } catch (Throwable e) {
+ PlotSquared.debug(
+ "Incompatible version of WorldEdit, please upgrade: http://builds.enginehub.org/job/worldedit?branch=master");
+ }
+ }
+ // Economy
+ if (Settings.Enabled_Components.ECONOMY) {
+ TaskManager
+ .runTask(() -> EconHandler.manager = PlotSquared.this.IMP.getEconomyHandler());
+ }
+
+/* // Check for updates
+ if (Settings.Enabled_Components.UPDATER) {
+ updater = new Updater();
+ TaskManager.IMP.taskAsync(new Runnable() {
+ @Override public void run() {
+ updater.update(getPlatform(), getVersion());
+ }
+ });
+ TaskManager.IMP.taskRepeatAsync(new Runnable() {
+ @Override public void run() {
+ updater.update(getPlatform(), getVersion());
+ }
+ }, 36000);
+ }*/
+
+ // World generators:
+ final ConfigurationSection section = this.worlds.getConfigurationSection("worlds");
+ if (section != null) {
+ for (String world : section.getKeys(false)) {
+ if (world.equals("CheckingPlotSquaredGenerator")) {
+ continue;
+ }
+ if (WorldUtil.IMP.isWorld(world)) {
+ this.IMP.setGenerator(world);
+ }
+ }
+ TaskManager.runTaskLater(() -> {
+ for (String world : section.getKeys(false)) {
+ if (world.equals("CheckingPlotSquaredGenerator")) {
+ continue;
+ }
+ if (!WorldUtil.IMP.isWorld(world) && !world.equals("*")) {
+ debug(
+ "&c`" + world + "` was not properly loaded - " + IMP.getPluginName()
+ + " will now try to load it properly: ");
+ debug(
+ "&8 - &7Are you trying to delete this world? Remember to remove it from the settings.yml, bukkit.yml and multiverse worlds.yml");
+ debug(
+ "&8 - &7Your world management plugin may be faulty (or non existent)");
+ PlotSquared.this.IMP.setGenerator(world);
+ }
+ }
+ }, 1);
+ }
+
+ // Copy files
+ copyFile("addplots.js", Settings.Paths.SCRIPTS);
+ copyFile("addsigns.js", Settings.Paths.SCRIPTS);
+ copyFile("automerge.js", Settings.Paths.SCRIPTS);
+ copyFile("furthest.js", Settings.Paths.SCRIPTS);
+ copyFile("mycommand.js", Settings.Paths.SCRIPTS);
+ copyFile("setbiomes.js", Settings.Paths.SCRIPTS);
+ copyFile("start.js", Settings.Paths.SCRIPTS);
+ copyFile("town.template", Settings.Paths.TEMPLATES);
+ copyFile("skyblock.template", Settings.Paths.TEMPLATES);
+ copyFile("bridge.template", Settings.Paths.TEMPLATES);
+ copyFile("de-DE.yml", Settings.Paths.TRANSLATIONS);
+ copyFile("es-ES.yml", Settings.Paths.TRANSLATIONS);
+ copyFile("zh-CN.yml", Settings.Paths.TRANSLATIONS);
+ copyFile("it-IT.yml", Settings.Paths.TRANSLATIONS);
+ copyFile("ko-KR.yml", Settings.Paths.TRANSLATIONS);
+ copyFile("fr-FR.yml", Settings.Paths.TRANSLATIONS);
+ showDebug();
+ } catch (Throwable e) {
+ e.printStackTrace();
+ }
+
+ PlotSquared.log(C.ENABLED.f(IMP.getPluginName()));
+ }
+
+ /**
+ * Get an instance of PlotSquared.
+ *
+ * @return instance of PlotSquared
+ */
+ public static PlotSquared get() {
+ return PlotSquared.instance;
+ }
+
+ public static IPlotMain imp() {
+ if (instance != null) {
+ return instance.IMP;
+ }
+ return null;
+ }
+
+ /**
+ * Log a message to the IPlotMain logger.
+ *
+ * @param message Message to log
+ * @see IPlotMain#log(String)
+ */
+ public static void log(Object message) {
+ if (message == null || message.toString().isEmpty()) {
+ return;
+ }
+ if (PlotSquared.get() == null || PlotSquared.get().getLogger() == null) {
+ System.out.printf("[P2][Info] %s\n", StringMan.getString(message));
+ } else {
+ PlotSquared.get().getLogger().log(StringMan.getString(message));
+ }
+ }
+
+ /**
+ * Log a message to the IPlotMain logger.
+ *
+ * @param message Message to log
+ * @see IPlotMain#log(String)
+ */
+ public static void debug(@Nullable Object message) {
+ if (Settings.DEBUG) {
+ if (PlotSquared.get() == null || PlotSquared.get().getLogger() == null) {
+ System.out.printf("[P2][Debug] %s\n", StringMan.getString(message));
+ } else {
+ PlotSquared.get().getLogger().log(StringMan.getString(message));
+ }
+ }
+ }
+
+ private void startUuidCatching() {
+ TaskManager.runTaskLater(() -> {
+ debug("Starting UUID caching");
+ UUIDHandler.startCaching(() -> {
+ UUIDHandler.add(new StringWrapper("*"), DBFunc.EVERYONE);
+ foreachPlotRaw(new RunnableVal() {
+ @Override public void run(Plot plot) {
+ if (plot.hasOwner() && plot.temp != -1) {
+ if (UUIDHandler.getName(plot.owner) == null) {
+ UUIDHandler.implementation.unknown.add(plot.owner);
+ }
+ }
+ }
+ });
+ startExpiryTasks();
+ });
+ }, 20);
+ }
+
+ private void startExpiryTasks() {
+ if (Settings.Enabled_Components.PLOT_EXPIRY) {
+ ExpireManager.IMP = new ExpireManager();
+ ExpireManager.IMP.runAutomatedTask();
+ for (Settings.Auto_Clear settings : Settings.AUTO_CLEAR.getInstances()) {
+ ExpiryTask task = new ExpiryTask(settings);
+ ExpireManager.IMP.addTask(task);
+ }
+ }
+ }
+
+ public boolean isMainThread(Thread thread) {
+ return this.thread == thread;
+ }
+
+ /**
+ * Check if `version` is >= `version2`.
+ *
+ * @param version First version
+ * @param version2 Second version
+ * @return true if `version` is >= `version2`
+ */
+ public boolean checkVersion(int[] version, int... version2) {
+ return version[0] > version2[0] || version[0] == version2[0] && version[1] > version2[1]
+ || version[0] == version2[0] && version[1] == version2[1] && version[2] >= version2[2];
+ }
+
+ /**
+ * Get the current PlotSquared version.
+ *
+ * @return current version in config or null
+ */
+ public PlotVersion getVersion() {
+ return this.version;
+ }
+
+ /**
+ * Get the server platform this plugin is running on this is running on.
+ *
+ *
This will be either Bukkit or Sponge
+ *
+ * @return the server implementation
+ */
+ public String getPlatform() {
+ return Settings.PLATFORM;
+ }
+
+ public PlotManager getPlotManager(Plot plot) {
+ return plot.getArea().manager;
+ }
+
+ public PlotManager getPlotManager(Location location) {
+ PlotArea pa = getPlotAreaAbs(location);
+ return pa != null ? pa.manager : null;
+ }
+
+ /**
+ * Add a global reference to a plot world.
+ *
+ * @param plotArea the {@code PlotArea} to add.
+ * @see #removePlotArea(PlotArea) To remove the reference
+ */
+ public void addPlotArea(PlotArea plotArea) {
+ HashMap plots;
+ if (plots_tmp == null || (plots = plots_tmp.remove(plotArea.toString())) == null) {
+ if (plotArea.TYPE == 2) {
+ plots = this.plots_tmp != null ? this.plots_tmp.get(plotArea.worldname) : null;
+ if (plots != null) {
+ Iterator> iterator = plots.entrySet().iterator();
+ while (iterator.hasNext()) {
+ Entry next = iterator.next();
+ PlotId id = next.getKey();
+ if (plotArea.contains(id)) {
+ next.getValue().setArea(plotArea);
+ iterator.remove();
+ }
+ }
+ }
+ }
+ } else {
+ for (Plot entry : plots.values()) {
+ entry.setArea(plotArea);
+ }
+ }
+ Set clusters;
+ if (clusters_tmp == null || (clusters = clusters_tmp.remove(plotArea.toString())) == null) {
+ if (plotArea.TYPE == 2) {
+ clusters =
+ this.clusters_tmp != null ? this.clusters_tmp.get(plotArea.worldname) : null;
+ if (clusters != null) {
+ Iterator iterator = clusters.iterator();
+ while (iterator.hasNext()) {
+ PlotCluster next = iterator.next();
+ if (next.intersects(plotArea.getMin(), plotArea.getMax())) {
+ next.setArea(plotArea);
+ iterator.remove();
+ }
+ }
+ }
+ }
+ } else {
+ for (PlotCluster cluster : clusters) {
+ cluster.setArea(plotArea);
+ }
+ }
+ plotAreaManager.addPlotArea(plotArea);
+ plotArea.setupBorder();
+ }
+
+ /**
+ * Remove a plot world reference.
+ *
+ * @param area the {@code PlotArea} to remove
+ */
+ public void removePlotArea(PlotArea area) {
+ plotAreaManager.removePlotArea(area);
+ setPlotsTmp(area);
+ }
+
+ public void removePlotAreas(String world) {
+ for (PlotArea area : getPlotAreas(world)) {
+ if (area.worldname.equals(world)) {
+ removePlotArea(area);
+ }
+ }
+ }
+
+ private void setPlotsTmp(PlotArea area) {
+ if (this.plots_tmp == null) {
+ this.plots_tmp = new HashMap<>();
+ }
+ HashMap map =
+ this.plots_tmp.computeIfAbsent(area.toString(), k -> new HashMap<>());
+ for (Plot plot : area.getPlots()) {
+ map.put(plot.getId(), plot);
+ }
+ if (this.clusters_tmp == null) {
+ this.clusters_tmp = new HashMap<>();
+ }
+ this.clusters_tmp.put(area.toString(), area.getClusters());
+ }
+
+ public Set getClusters(String world) {
+ Set set = new HashSet<>();
+ for (PlotArea area : getPlotAreas(world)) {
+ set.addAll(area.getClusters());
+ }
+ return Collections.unmodifiableSet(set);
+
+ }
+
+ /**
+ * Get all the base plots in a single set (for merged plots it just returns the bottom plot).
+ *
+ * @return Set of base Plots
+ */
+ public Set getBasePlots() {
+ int size = getPlotCount();
+ final Set result = new HashSet<>(size);
+ foreachPlotArea(new RunnableVal() {
+ @Override public void run(PlotArea value) {
+ for (Plot plot : value.getPlots()) {
+ if (!plot.isBasePlot()) {
+ continue;
+ }
+ result.add(plot);
+ }
+ }
+ });
+ return Collections.unmodifiableSet(result);
+ }
+
+ public List sortPlotsByTemp(Collection plots) {
+ int max = 0;
+ int overflowCount = 0;
+ for (Plot plot : plots) {
+ if (plot.temp > 0) {
+ if (plot.temp > max) {
+ max = plot.temp;
+ }
+ } else {
+ overflowCount++;
+ }
+ }
+ Plot[] array = new Plot[max + 1];
+ List overflow = new ArrayList<>(overflowCount);
+ for (Plot plot : plots) {
+ if (plot.temp <= 0) {
+ overflow.add(plot);
+ } else {
+ array[plot.temp] = plot;
+ }
+ }
+ ArrayList result = new ArrayList<>(plots.size());
+ for (Plot plot : array) {
+ if (plot != null) {
+ result.add(plot);
+ }
+ }
+ overflow.sort(Comparator.comparingInt(Plot::hashCode));
+ result.addAll(overflow);
+ return result;
+ }
+
+ /**
+ * Sort plots by hashcode.
+ *
+ * @param plots the collection of plots to sort
+ * @return the sorted collection {@link #sortPlots(Collection, SortType, PlotArea)} which has
+ * additional checks before calling this
+ */
+ // TODO: Re-evaluate this (previously public and deprecated), as it's being used internally
+ private ArrayList sortPlotsByHash(Collection plots) {
+ int hardmax = 256000;
+ int max = 0;
+ int overflowSize = 0;
+ for (Plot plot : plots) {
+ int hash = MathMan.getPositiveId(plot.hashCode());
+ if (hash > max) {
+ if (hash >= hardmax) {
+ overflowSize++;
+ } else {
+ max = hash;
+ }
+ }
+ }
+ hardmax = Math.min(hardmax, max);
+ Plot[] cache = new Plot[hardmax + 1];
+ List overflow = new ArrayList<>(overflowSize);
+ ArrayList extra = new ArrayList<>();
+ for (Plot plot : plots) {
+ int hash = MathMan.getPositiveId(plot.hashCode());
+ if (hash < hardmax) {
+ if (hash >= 0) {
+ cache[hash] = plot;
+ } else {
+ extra.add(plot);
+ }
+ } else if (Math.abs(plot.getId().x) > 15446 || Math.abs(plot.getId().y) > 15446) {
+ extra.add(plot);
+ } else {
+ overflow.add(plot);
+ }
+ }
+ Plot[] overflowArray = overflow.toArray(new Plot[overflow.size()]);
+ sortPlotsByHash(overflowArray);
+ ArrayList result = new ArrayList<>(cache.length + overflowArray.length);
+ for (Plot plot : cache) {
+ if (plot != null) {
+ result.add(plot);
+ }
+ }
+ Collections.addAll(result, overflowArray);
+ result.addAll(extra);
+ return result;
+ }
+
+ private void sortPlotsByHash(Plot[] input) {
+ List[] bucket = new ArrayList[32];
+ for (int i = 0; i < bucket.length; i++) {
+ bucket[i] = new ArrayList<>();
+ }
+ boolean maxLength = false;
+ int placement = 1;
+ while (!maxLength) {
+ maxLength = true;
+ for (Plot i : input) {
+ int tmp = MathMan.getPositiveId(i.hashCode()) / placement;
+ bucket[tmp & 31].add(i);
+ if (maxLength && tmp > 0) {
+ maxLength = false;
+ }
+ }
+ int a = 0;
+ for (int b = 0; b < 32; b++) {
+ for (Plot i : bucket[b]) {
+ input[a++] = i;
+ }
+ bucket[b].clear();
+ }
+ placement *= 32;
+ }
+ }
+
+ private ArrayList sortPlotsByTimestamp(Collection plots) {
+ int hardMax = 256000;
+ int max = 0;
+ int overflowSize = 0;
+ for (Plot plot : plots) {
+ int hash = MathMan.getPositiveId(plot.hashCode());
+ if (hash > max) {
+ if (hash >= hardMax) {
+ overflowSize++;
+ } else {
+ max = hash;
+ }
+ }
+ }
+ hardMax = Math.min(hardMax, max);
+ Plot[] cache = new Plot[hardMax + 1];
+ List overflow = new ArrayList<>(overflowSize);
+ ArrayList extra = new ArrayList<>();
+ for (Plot plot : plots) {
+ int hash = MathMan.getPositiveId(plot.hashCode());
+ if (hash < hardMax) {
+ if (hash >= 0) {
+ cache[hash] = plot;
+ } else {
+ extra.add(plot);
+ }
+ } else if (Math.abs(plot.getId().x) > 15446 || Math.abs(plot.getId().y) > 15446) {
+ extra.add(plot);
+ } else {
+ overflow.add(plot);
+ }
+ }
+ Plot[] overflowArray = overflow.toArray(new Plot[overflow.size()]);
+ sortPlotsByHash(overflowArray);
+ ArrayList result = new ArrayList<>(cache.length + overflowArray.length);
+ for (Plot plot : cache) {
+ if (plot != null) {
+ result.add(plot);
+ }
+ }
+ Collections.addAll(result, overflowArray);
+ result.addAll(extra);
+ return result;
+ }
+
+ /**
+ * Sort plots by creation timestamp.
+ */
+
+ private List sortPlotsByModified(Collection input) {
+ List list;
+ if (input instanceof List) {
+ list = (List) input;
+ } else {
+ list = new ArrayList<>(input);
+ }
+ list.sort(Comparator.comparingLong(a -> ExpireManager.IMP.getTimestamp(a.owner)));
+ return list;
+ }
+
+ /**
+ * Sort a collection of plots by world (with a priority world), then by hashcode.
+ *
+ * @param plots the plots to sort
+ * @param type The sorting method to use for each world (timestamp, or hash)
+ * @param priorityArea Use null, "world", or "gibberish" if you want default world order
+ * @return ArrayList of plot
+ */
+ public ArrayList sortPlots(Collection plots, SortType type,
+ final PlotArea priorityArea) {
+ // group by world
+ // sort each
+ HashMap> map = new HashMap<>();
+ int totalSize = getPlotCount();
+ if (plots.size() == totalSize) {
+ for (PlotArea area : plotAreaManager.getAllPlotAreas()) {
+ map.put(area, area.getPlots());
+ }
+ } else {
+ for (PlotArea area : plotAreaManager.getAllPlotAreas()) {
+ map.put(area, new ArrayList<>(0));
+ }
+ Collection lastList = null;
+ PlotArea lastWorld = null;
+ for (Plot plot : plots) {
+ if (lastWorld == plot.getArea()) {
+ lastList.add(plot);
+ } else {
+ lastWorld = plot.getArea();
+ lastList = map.get(lastWorld);
+ lastList.add(plot);
+ }
+ }
+ }
+ List areas = Arrays.asList(plotAreaManager.getAllPlotAreas());
+ areas.sort((a, b) -> {
+ if (priorityArea != null) {
+ if (a.equals(priorityArea)) {
+ return -1;
+ } else if (b.equals(priorityArea)) {
+ return 1;
+ }
+ }
+ return a.hashCode() - b.hashCode();
+ });
+ ArrayList toReturn = new ArrayList<>(plots.size());
+ for (PlotArea area : areas) {
+ switch (type) {
+ case CREATION_DATE:
+ toReturn.addAll(sortPlotsByTemp(map.get(area)));
+ break;
+ case CREATION_DATE_TIMESTAMP:
+ toReturn.addAll(sortPlotsByTimestamp(map.get(area)));
+ break;
+ case DISTANCE_FROM_ORIGIN:
+ toReturn.addAll(sortPlotsByHash(map.get(area)));
+ break;
+ case LAST_MODIFIED:
+ toReturn.addAll(sortPlotsByModified(map.get(area)));
+ break;
+ default:
+ break;
+ }
+ }
+ return toReturn;
+ }
+
+ /**
+ * A more generic way to filter plots - make your own method if you need complex filters.
+ *
+ * @param filters the filter
+ * @return a filtered set of plots
+ */
+ public Set getPlots(final PlotFilter... filters) {
+ final HashSet set = new HashSet<>();
+ foreachPlotArea(new RunnableVal() {
+ @Override public void run(PlotArea value) {
+ for (PlotFilter filter : filters) {
+ if (!filter.allowsArea(value)) {
+ return;
+ }
+ }
+ loop:
+ for (Entry entry2 : value.getPlotEntries()) {
+ Plot plot = entry2.getValue();
+ for (PlotFilter filter : filters) {
+ if (!filter.allowsPlot(plot)) {
+ continue loop;
+ }
+ }
+ set.add(plot);
+ }
+ }
+ });
+ return set;
+ }
+
+ /**
+ * Get all the plots in a single set.
+ *
+ * @return Set of Plots
+ */
+ public Set getPlots() {
+ int size = getPlotCount();
+ final Set result = new HashSet<>(size);
+ foreachPlotArea(new RunnableVal() {
+ @Override public void run(PlotArea value) {
+ result.addAll(value.getPlots());
+ }
+ });
+ return result;
+ }
+
+ //TODO look at uncommenting or deleting tis. Is it useful and will it work?
+ //If it is readded make sure there is proper javadoc documentation for it.
+/*
+ public void setPlots(HashMap> plots) {
+ if (this.plots_tmp == null) {
+ this.plots_tmp = new HashMap<>();
+ }
+ for (Entry> entry : plots.entrySet()) {
+ String world = entry.getKey();
+ PlotArea area = getPlotArea(world, null);
+ if (area == null) {
+ HashMap map = this.plots_tmp
+ .computeIfAbsent(world, k -> new HashMap<>());
+ map.putAll(entry.getValue());
+ } else {
+ for (Plot plot : entry.getValue().values()) {
+ plot.setArea(area);
+ area.addPlot(plot);
+ }
+ }
+ }
+ }
+*/
+
+ /**
+ * Get all the plots owned by a player name.
+ *
+ * @param world the world
+ * @param player the plot owner
+ * @return Set of Plot
+ */
+ public Set getPlots(String world, String player) {
+ final UUID uuid = UUIDHandler.getUUID(player, null);
+ return getPlots(world, uuid);
+ }
+
+ /**
+ * Get all the plots owned by a player name.
+ *
+ * @param area the PlotArea
+ * @param player the plot owner
+ * @return Set of Plot
+ */
+ public Set getPlots(PlotArea area, String player) {
+ UUID uuid = UUIDHandler.getUUID(player, null);
+ return getPlots(area, uuid);
+ }
+
+ /**
+ * Get all plots by a PlotPlayer.
+ *
+ * @param world the world
+ * @param player the plot owner
+ * @return Set of plot
+ */
+ public Set getPlots(String world, PlotPlayer player) {
+ return getPlots(world, player.getUUID());
+ }
+
+ /**
+ * Get all plots by a PlotPlayer.
+ *
+ * @param area the PlotArea
+ * @param player the plot owner
+ * @return Set of plot
+ */
+ public Set getPlots(PlotArea area, PlotPlayer player) {
+ return getPlots(area, player.getUUID());
+ }
+
+ /**
+ * Get all plots by a UUID in a world.
+ *
+ * @param world the world
+ * @param uuid the plot owner
+ * @return Set of plot
+ */
+ public Set getPlots(String world, UUID uuid) {
+ final Set plots = new HashSet<>();
+ for (final Plot plot : getPlots(world)) {
+ if (plot.hasOwner() && plot.isOwnerAbs(uuid)) {
+ plots.add(plot);
+ }
+ }
+ return Collections.unmodifiableSet(plots);
+ }
+
+ /**
+ * Get all plots by a UUID in an area.
+ *
+ * @param area the {@code PlotArea}
+ * @param uuid the plot owner
+ * @return Set of plots
+ */
+ public Set getPlots(PlotArea area, UUID uuid) {
+ final Set plots = new HashSet<>();
+ for (Plot plot : getPlots(area)) {
+ if (plot.hasOwner() && plot.isOwnerAbs(uuid)) {
+ plots.add(plot);
+ }
+ }
+ return Collections.unmodifiableSet(plots);
+ }
+
+ /**
+ * Check if a plot world.
+ *
+ * @param world the world
+ * @return if a plot world is registered
+ * @see #getPlotAreaByString(String) to get the PlotArea object
+ */
+ public boolean hasPlotArea(String world) {
+ return plotAreaManager.getPlotAreas(world, null).length != 0;
+ }
+
+ public Collection getPlots(String world) {
+ final Set set = new HashSet<>();
+ foreachPlotArea(world, new RunnableVal() {
+ @Override public void run(PlotArea value) {
+ set.addAll(value.getPlots());
+ }
+ });
+ return set;
+ }
+
+ /**
+ * Get the plots for a PlotPlayer.
+ *
+ * @param player the player to retrieve the plots for
+ * @return Set of Plot
+ */
+ public Set getPlots(PlotPlayer player) {
+ return getPlots(player.getUUID());
+ }
+
+ public Collection getPlots(PlotArea area) {
+ return area == null ? EMPTY_SET : area.getPlots();
+ }
+
+ public Plot getPlot(PlotArea area, PlotId id) {
+ return area == null ? null : id == null ? null : area.getPlot(id);
+ }
+
+ public Set getBasePlots(PlotPlayer player) {
+ return getBasePlots(player.getUUID());
+ }
+
+ /**
+ * Get the plots for a UUID.
+ *
+ * @param uuid the plot owner
+ * @return Set of Plot's owned by the player
+ */
+ public Set getPlots(final UUID uuid) {
+ final Set plots = new HashSet<>();
+ foreachPlot(new RunnableVal() {
+ @Override public void run(Plot value) {
+ if (value.isOwnerAbs(uuid)) {
+ plots.add(value);
+ }
+ }
+ });
+ return Collections.unmodifiableSet(plots);
+ }
+
+ public boolean hasPlot(final UUID uuid) {
+ for (final PlotArea area : plotAreaManager.getAllPlotAreas()) {
+ if (area.hasPlot(uuid)) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ public Set getBasePlots(final UUID uuid) {
+ final Set plots = new HashSet<>();
+ foreachBasePlot(new RunnableVal() {
+ @Override public void run(Plot value) {
+ if (value.isOwner(uuid)) {
+ plots.add(value);
+ }
+ }
+ });
+ return Collections.unmodifiableSet(plots);
+ }
+
+ /**
+ * Get the plots for a UUID.
+ *
+ * @param uuid the UUID of the owner
+ * @return Set of Plot
+ */
+ public Set getPlotsAbs(final UUID uuid) {
+ final Set plots = new HashSet<>();
+ foreachPlot(new RunnableVal() {
+ @Override public void run(Plot value) {
+ if (value.isOwnerAbs(uuid)) {
+ plots.add(value);
+ }
+ }
+ });
+ return Collections.unmodifiableSet(plots);
+ }
+
+ /**
+ * Unregister a plot from local memory (does not call DB).
+ *
+ * @param plot the plot to remove
+ * @param callEvent If to call an event about the plot being removed
+ * @return true if plot existed | false if it didn't
+ */
+ public boolean removePlot(Plot plot, boolean callEvent) {
+ if (plot == null) {
+ return false;
+ }
+ if (callEvent) {
+ EventUtil.manager.callDelete(plot);
+ }
+ if (plot.getArea().removePlot(plot.getId())) {
+ PlotId last = (PlotId) plot.getArea().getMeta("lastPlot");
+ int last_max = Math.max(Math.abs(last.x), Math.abs(last.y));
+ int this_max = Math.max(Math.abs(plot.getId().x), Math.abs(plot.getId().y));
+ if (this_max < last_max) {
+ plot.getArea().setMeta("lastPlot", plot.getId());
+ }
+ return true;
+ }
+ return false;
+ }
+
+ /**
+ * This method is called by the PlotGenerator class normally.
+ *
+ * Initializes the PlotArea and PlotManager classes
+ * Registers the PlotArea and PlotManager classes
+ * Loads (and/or generates) the PlotArea configuration
+ * Sets up the world border if configured
+ *
+ *
+ * If loading an augmented plot world:
+ *
+ * Creates the AugmentedPopulator classes
+ * Injects the AugmentedPopulator classes if required
+ *
+ *
+ * @param world the world to load
+ * @param baseGenerator The generator for that world, or null
+ */
+ public void loadWorld(String world, GeneratorWrapper> baseGenerator) {
+ if (world.equals("CheckingPlotSquaredGenerator")) {
+ return;
+ }
+ this.plotAreaManager.addWorld(world);
+ Set worlds;
+ if (this.worlds.contains("worlds")) {
+ worlds = this.worlds.getConfigurationSection("worlds").getKeys(false);
+ } else {
+ worlds = new HashSet<>();
+ }
+ String path = "worlds." + world;
+ ConfigurationSection worldSection = this.worlds.getConfigurationSection(path);
+ int type;
+ if (worldSection != null) {
+ type = worldSection.getInt("generator.type", 0);
+ } else {
+ type = 0;
+ }
+ if (type == 0) {
+ if (plotAreaManager.getPlotAreas(world, null).length != 0) {
+ debug("World possibly already loaded: " + world);
+ return;
+ }
+ IndependentPlotGenerator plotGenerator;
+ if (baseGenerator != null && baseGenerator.isFull()) {
+ plotGenerator = baseGenerator.getPlotGenerator();
+ } else if (worldSection != null) {
+ String secondaryGeneratorName = worldSection.getString("generator.plugin");
+ GeneratorWrapper> secondaryGenerator =
+ this.IMP.getGenerator(world, secondaryGeneratorName);
+ if (secondaryGenerator != null && secondaryGenerator.isFull()) {
+ plotGenerator = secondaryGenerator.getPlotGenerator();
+ } else {
+ String primaryGeneratorName = worldSection.getString("generator.init");
+ GeneratorWrapper> primaryGenerator =
+ this.IMP.getGenerator(world, primaryGeneratorName);
+ if (primaryGenerator != null && primaryGenerator.isFull()) {
+ plotGenerator = primaryGenerator.getPlotGenerator();
+ } else {
+ return;
+ }
+ }
+ } else {
+ return;
+ }
+ // Conventional plot generator
+ PlotArea plotArea = plotGenerator.getNewPlotArea(world, null, null, null);
+ PlotManager plotManager = plotGenerator.getNewPlotManager();
+ PlotSquared.log(C.PREFIX + "&aDetected world load for '" + world + "'");
+ PlotSquared.log(C.PREFIX + "&3 - generator: &7" + baseGenerator + ">" + plotGenerator);
+ PlotSquared.log(C.PREFIX + "&3 - plotworld: &7" + plotArea.getClass().getName());
+ PlotSquared
+ .log(C.PREFIX + "&3 - plotAreaManager: &7" + plotManager.getClass().getName());
+ if (!this.worlds.contains(path)) {
+ this.worlds.createSection(path);
+ worldSection = this.worlds.getConfigurationSection(path);
+ }
+ plotArea.saveConfiguration(worldSection);
+ plotArea.loadDefaultConfiguration(worldSection);
+ try {
+ this.worlds.save(this.worldsFile);
+ } catch (IOException e) {
+ e.printStackTrace();
+ }
+ // Now add it
+ addPlotArea(plotArea);
+ plotGenerator.initialize(plotArea);
+ } else {
+ if (!worlds.contains(world)) {
+ return;
+ }
+ ConfigurationSection areasSection = worldSection.getConfigurationSection("areas");
+ if (areasSection == null) {
+ if (plotAreaManager.getPlotAreas(world, null).length != 0) {
+ debug("World possibly already loaded: " + world);
+ return;
+ }
+ PlotSquared.log(C.PREFIX + "&aDetected world load for '" + world + "'");
+ String gen_string = worldSection.getString("generator.plugin", IMP.getPluginName());
+ if (type == 2) {
+ Set clusters =
+ this.clusters_tmp != null ? this.clusters_tmp.get(world) : new HashSet<>();
+ if (clusters == null) {
+ throw new IllegalArgumentException("No cluster exists for world: " + world);
+ }
+ ArrayDeque toLoad = new ArrayDeque<>();
+ for (PlotCluster cluster : clusters) {
+ PlotId pos1 = cluster.getP1(); // Cluster pos1
+ PlotId pos2 = cluster.getP2(); // Cluster pos2
+ String name = cluster.getName(); // Cluster name
+ String fullId = name + "-" + pos1 + "-" + pos2;
+ worldSection.createSection("areas." + fullId);
+ DBFunc.replaceWorld(world, world + ";" + name, pos1, pos2); // NPE
+
+ PlotSquared.log(C.PREFIX + "&3 - " + name + "-" + pos1 + "-" + pos2);
+ GeneratorWrapper> areaGen = this.IMP.getGenerator(world, gen_string);
+ if (areaGen == null) {
+ throw new IllegalArgumentException("Invalid Generator: " + gen_string);
+ }
+ PlotArea pa =
+ areaGen.getPlotGenerator().getNewPlotArea(world, name, pos1, pos2);
+ pa.saveConfiguration(worldSection);
+ pa.loadDefaultConfiguration(worldSection);
+ try {
+ this.worlds.save(this.worldsFile);
+ } catch (IOException e) {
+ e.printStackTrace();
+ }
+ PlotSquared
+ .log(C.PREFIX + "&c | &9generator: &7" + baseGenerator + ">" + areaGen);
+ PlotSquared.log(C.PREFIX + "&c | &9plotworld: &7" + pa);
+ PlotSquared.log(C.PREFIX + "&c | &9manager: &7" + pa);
+ PlotSquared.log(C.PREFIX + "&cNote: &7Area created for cluster:" + name
+ + " (invalid or old configuration?)");
+ areaGen.getPlotGenerator().initialize(pa);
+ areaGen.augment(pa);
+ toLoad.add(pa);
+ }
+ for (PlotArea area : toLoad) {
+ addPlotArea(area);
+ }
+ return;
+ }
+ GeneratorWrapper> areaGen = this.IMP.getGenerator(world, gen_string);
+ if (areaGen == null) {
+ throw new IllegalArgumentException("Invalid Generator: " + gen_string);
+ }
+ PlotArea pa = areaGen.getPlotGenerator().getNewPlotArea(world, null, null, null);
+ pa.saveConfiguration(worldSection);
+ pa.loadDefaultConfiguration(worldSection);
+ try {
+ this.worlds.save(this.worldsFile);
+ } catch (IOException e) {
+ e.printStackTrace();
+ }
+ PlotSquared.log(C.PREFIX + "&3 - generator: &7" + baseGenerator + ">" + areaGen);
+ PlotSquared.log(C.PREFIX + "&3 - plotworld: &7" + pa);
+ PlotSquared.log(C.PREFIX + "&3 - plotAreaManager: &7" + pa.getPlotManager());
+ areaGen.getPlotGenerator().initialize(pa);
+ areaGen.augment(pa);
+ addPlotArea(pa);
+ return;
+ }
+ if (type == 1) {
+ throw new IllegalArgumentException(
+ "Invalid type for multi-area world. Expected `2`, got `" + 1 + "`");
+ }
+ for (String areaId : areasSection.getKeys(false)) {
+ PlotSquared.log(C.PREFIX + "&3 - " + areaId);
+ String[] split = areaId.split("(?<=[^;-])-");
+ if (split.length != 3) {
+ throw new IllegalArgumentException("Invalid Area identifier: " + areaId
+ + ". Expected form `--`");
+ }
+ String name = split[0];
+ PlotId pos1 = PlotId.fromString(split[1]);
+ PlotId pos2 = PlotId.fromString(split[2]);
+ if (pos1 == null || pos2 == null || name.isEmpty()) {
+ throw new IllegalArgumentException("Invalid Area identifier: " + areaId
+ + ". Expected form `--`");
+ }
+ PlotArea existing = getPlotArea(world, name);
+ if (existing != null && name.equals(existing.id)) {
+ continue;
+ }
+ ConfigurationSection section = areasSection.getConfigurationSection(areaId);
+ YamlConfiguration clone = new YamlConfiguration();
+ for (String key : section.getKeys(true)) {
+ if (section.get(key) instanceof MemorySection) {
+ continue;
+ }
+ if (!clone.contains(key)) {
+ clone.set(key, section.get(key));
+ }
+ }
+ for (String key : worldSection.getKeys(true)) {
+ if (worldSection.get(key) instanceof MemorySection) {
+ continue;
+ }
+ if (!key.startsWith("areas") && !clone.contains(key)) {
+ clone.set(key, worldSection.get(key));
+ }
+ }
+ String gen_string = clone.getString("generator.plugin", IMP.getPluginName());
+ GeneratorWrapper> areaGen = this.IMP.getGenerator(world, gen_string);
+ if (areaGen == null) {
+ throw new IllegalArgumentException("Invalid Generator: " + gen_string);
+ }
+ PlotArea pa = areaGen.getPlotGenerator().getNewPlotArea(world, name, pos1, pos2);
+ pa.saveConfiguration(clone);
+ // netSections is the combination of
+ for (String key : clone.getKeys(true)) {
+ if (clone.get(key) instanceof MemorySection) {
+ continue;
+ }
+ if (!worldSection.contains(key)) {
+ worldSection.set(key, clone.get(key));
+ } else {
+ Object value = worldSection.get(key);
+ if (!Objects.equals(value, clone.get(key))) {
+ section.set(key, clone.get(key));
+ }
+ }
+ }
+ pa.loadDefaultConfiguration(clone);
+ try {
+ this.worlds.save(this.worldsFile);
+ } catch (IOException e) {
+ e.printStackTrace();
+ }
+ PlotSquared.log(C.PREFIX + "&aDetected area load for '" + world + "'");
+ PlotSquared.log(C.PREFIX + "&c | &9generator: &7" + baseGenerator + ">" + areaGen);
+ PlotSquared.log(C.PREFIX + "&c | &9plotworld: &7" + pa);
+ PlotSquared.log(C.PREFIX + "&c | &9manager: &7" + pa.getPlotManager());
+ areaGen.getPlotGenerator().initialize(pa);
+ areaGen.augment(pa);
+ addPlotArea(pa);
+ }
+ }
+ }
+
+ /**
+ * Setup the configuration for a plot world based on world arguments.
+ *
+ *
+ * e.g. /mv create <world> normal -g PlotSquared:<args>
+ *
+ * @param world The name of the world
+ * @param args The arguments
+ * @param generator the plot generator
+ * @return boolean | if valid arguments were provided
+ */
+ public boolean setupPlotWorld(String world, String args, IndependentPlotGenerator generator) {
+ if (args != null && !args.isEmpty()) {
+ // save configuration
+
+ final List validArguments = Arrays
+ .asList("s=", "size=", "g=", "gap=", "h=", "height=", "f=", "floor=", "m=", "main=",
+ "w=", "wall=", "b=", "border=");
+
+ // Calculate the number of expected arguments
+ int expected = 0;
+ for (final String validArgument : validArguments) {
+ if (args.toLowerCase(Locale.ENGLISH).contains(validArgument)) {
+ expected += 1;
+ }
+ }
+
+ String[] split = args.toLowerCase(Locale.ENGLISH).split(",");
+
+ if (split.length > expected) {
+ // This means we have multi-block block buckets
+ String[] combinedArgs = new String[expected];
+ int index = 0;
+
+ StringBuilder argBuilder = new StringBuilder();
+ outer:
+ for (final String string : split) {
+ for (final String validArgument : validArguments) {
+ if (string.contains(validArgument)) {
+ if (!argBuilder.toString().isEmpty()) {
+ combinedArgs[index++] = argBuilder.toString();
+ argBuilder = new StringBuilder();
+ }
+ argBuilder.append(string);
+ continue outer;
+ }
+ }
+ if (argBuilder.toString().charAt(argBuilder.length() - 1) != '=') {
+ argBuilder.append(",");
+ }
+ argBuilder.append(string);
+ }
+
+ if (!argBuilder.toString().isEmpty()) {
+ combinedArgs[index] = argBuilder.toString();
+ }
+
+ split = combinedArgs;
+ }
+
+ HybridPlotWorld plotworld = new HybridPlotWorld(world, null, generator, null, null);
+ for (String element : split) {
+ String[] pair = element.split("=");
+ if (pair.length != 2) {
+ PlotSquared.log("&cNo value provided for: &7" + element);
+ return false;
+ }
+ String key = pair[0].toLowerCase();
+ String value = pair[1];
+ String base = "worlds." + world + ".";
+ try {
+ switch (key) {
+ case "s":
+ case "size":
+ this.worlds.set(base + "plot.size",
+ Configuration.INTEGER.parseString(value).shortValue());
+ break;
+ case "g":
+ case "gap":
+ this.worlds.set(base + "road.width",
+ Configuration.INTEGER.parseString(value).shortValue());
+ break;
+ case "h":
+ case "height":
+ this.worlds.set(base + "road.height",
+ Configuration.INTEGER.parseString(value).shortValue());
+ this.worlds.set(base + "plot.height",
+ Configuration.INTEGER.parseString(value).shortValue());
+ this.worlds.set(base + "wall.height",
+ Configuration.INTEGER.parseString(value).shortValue());
+ break;
+ case "f":
+ case "floor":
+ this.worlds.set(base + "plot.floor",
+ Configuration.BLOCK_BUCKET.parseString(value).toString());
+ break;
+ case "m":
+ case "main":
+ this.worlds.set(base + "plot.filling",
+ Configuration.BLOCK_BUCKET.parseString(value).toString());
+ break;
+ case "w":
+ case "wall":
+ this.worlds.set(base + "wall.filling",
+ Configuration.BLOCK_BUCKET.parseString(value).toString());
+ break;
+ case "b":
+ case "border":
+ this.worlds.set(base + "wall.block",
+ Configuration.BLOCK_BUCKET.parseString(value).toString());
+ break;
+ default:
+ PlotSquared.log("&cKey not found: &7" + element);
+ return false;
+ }
+ } catch (Exception e) {
+ e.printStackTrace();
+ PlotSquared.log("&cInvalid value: &7" + value + " in arg " + element);
+ return false;
+ }
+ }
+ try {
+ ConfigurationSection section =
+ this.worlds.getConfigurationSection("worlds." + world);
+ plotworld.saveConfiguration(section);
+ plotworld.loadConfiguration(section);
+ this.worlds.save(this.worldsFile);
+ } catch (IOException e) {
+ e.printStackTrace();
+ }
+ }
+ return true;
+ }
+
+ public boolean canUpdate(@NonNull final String current, @NonNull final String other) {
+ final String s1 = normalisedVersion(current);
+ final String s2 = normalisedVersion(other);
+ return s1.compareTo(s2) < 0;
+ }
+
+ public String normalisedVersion(@NonNull final String version) {
+ final String[] split = Pattern.compile(".", Pattern.LITERAL).split(version);
+ final StringBuilder sb = new StringBuilder();
+ for (final String s : split) {
+ sb.append(String.format("%" + 4 + 's', s));
+ }
+ return sb.toString();
+ }
+
+ public boolean update(PlotPlayer sender, URL url) {
+ try {
+ String name = this.jarFile.getName();
+ File newJar = new File("plugins/update/" + name);
+ MainUtil.sendMessage(sender, "$1Downloading from provided URL: &7" + url);
+ URLConnection con = url.openConnection();
+ try (InputStream stream = con.getInputStream()) {
+ File parent = newJar.getParentFile();
+ if (!parent.exists()) {
+ parent.mkdirs();
+ }
+ MainUtil.sendMessage(sender, "$2 - Output: " + newJar);
+ if (!newJar.delete()) {
+ MainUtil.sendMessage(sender, "Failed to update " + IMP.getPluginName() + "");
+ MainUtil.sendMessage(sender, "Jar file failed to delete.");
+ MainUtil.sendMessage(sender, " - Please update manually");
+ }
+ Files.copy(stream, newJar.toPath());
+ }
+ MainUtil.sendMessage(sender,
+ "$1The update will take effect when the server is restarted next");
+ return true;
+ } catch (IOException e) {
+ MainUtil.sendMessage(sender, "Failed to update " + IMP.getPluginName() + "");
+ MainUtil.sendMessage(sender, " - Please update manually");
+ PlotSquared.log("============ Stacktrace ============");
+ e.printStackTrace();
+ PlotSquared.log("====================================");
+ }
+ return false;
+ }
+
+ /**
+ * Copy a file from inside the jar to a location
+ *
+ * @param file Name of the file inside PlotSquared.jar
+ * @param folder The output location relative to /plugins/PlotSquared/
+ */
+ public void copyFile(String file, String folder) {
+ try {
+ File output = this.IMP.getDirectory();
+ if (!output.exists()) {
+ output.mkdirs();
+ }
+ File newFile = MainUtil.getFile(output, folder + File.separator + file);
+ if (newFile.exists()) {
+ return;
+ }
+ try (InputStream stream = this.IMP.getClass().getResourceAsStream(file)) {
+ byte[] buffer = new byte[2048];
+ if (stream == null) {
+ try (ZipInputStream zis = new ZipInputStream(
+ new FileInputStream(this.jarFile))) {
+ ZipEntry ze = zis.getNextEntry();
+ while (ze != null) {
+ String name = ze.getName();
+ if (name.equals(file)) {
+ new File(newFile.getParent()).mkdirs();
+ try (FileOutputStream fos = new FileOutputStream(newFile)) {
+ int len;
+ while ((len = zis.read(buffer)) > 0) {
+ fos.write(buffer, 0, len);
+ }
+ }
+ ze = null;
+ } else {
+ ze = zis.getNextEntry();
+ }
+ }
+ zis.closeEntry();
+ }
+ return;
+ }
+ newFile.createNewFile();
+ try (FileOutputStream fos = new FileOutputStream(newFile)) {
+ int len;
+ while ((len = stream.read(buffer)) > 0) {
+ fos.write(buffer, 0, len);
+ }
+ }
+ }
+ } catch (IOException e) {
+ e.printStackTrace();
+ PlotSquared.log("&cCould not save " + file);
+ }
+ }
+
+ private Map> getPlotsRaw() {
+ HashMap> map = new HashMap<>();
+ for (PlotArea area : this.plotAreaManager.getAllPlotAreas()) {
+ Map map2 = map.get(area.toString());
+ if (map2 == null) {
+ map.put(area.toString(), area.getPlotsRaw());
+ } else {
+ map2.putAll(area.getPlotsRaw());
+ }
+ }
+ return map;
+ }
+
+ /**
+ * Close the database connection.
+ */
+ public void disable() {
+ try {
+ // Validate that all data in the db is correct
+ final HashSet plots = new HashSet<>();
+ try {
+ foreachPlotRaw(new RunnableVal() {
+ @Override public void run(Plot value) {
+ plots.add(value);
+ }
+ });
+ } catch (final Exception ignored) {
+ }
+ DBFunc.validatePlots(plots);
+
+ // Close the connection
+ DBFunc.close();
+ UUIDHandler.handleShutdown();
+ } catch (NullPointerException exception) {
+ exception.printStackTrace();
+ PlotSquared.log("&cCould not close database connection!");
+ }
+ }
+
+ /**
+ * Setup the database connection.
+ */
+ public void setupDatabase() {
+ try {
+ if (DBFunc.dbManager != null) {
+ DBFunc.dbManager.close();
+ }
+ Database database;
+ if (Storage.MySQL.USE) {
+ database = new MySQL(Storage.MySQL.HOST, Storage.MySQL.PORT, Storage.MySQL.DATABASE,
+ Storage.MySQL.USER, Storage.MySQL.PASSWORD);
+ } else if (Storage.SQLite.USE) {
+ File file = MainUtil.getFile(IMP.getDirectory(), Storage.SQLite.DB + ".db");
+ database = new SQLite(file);
+ } else {
+ PlotSquared.log(C.PREFIX + "&cNo storage type is set!");
+ this.IMP.disable();
+ return;
+ }
+ DBFunc.dbManager = new SQLManager(database, Storage.PREFIX, false);
+ PlotSquared.log("GETTING PLOTS NOW AND STORING TO PLOTS_TMP");
+ for (PlotArea allPlotArea : plotAreaManager.getAllPlotAreas()) {
+ PlotSquared.log(allPlotArea.toString());
+ }
+
+ this.plots_tmp = DBFunc.getPlots();
+ if (plotAreaManager instanceof SinglePlotAreaManager) {
+ SinglePlotArea area = ((SinglePlotAreaManager) plotAreaManager).getArea();
+ addPlotArea(area);
+ ConfigurationSection section = worlds.getConfigurationSection("worlds.*");
+ if (section == null) {
+ section = worlds.createSection("worlds.*");
+ }
+ area.saveConfiguration(section);
+ area.loadDefaultConfiguration(section);
+ }
+ this.clusters_tmp = DBFunc.getClusters();
+ } catch (ClassNotFoundException | SQLException e) {
+ PlotSquared.log(
+ C.PREFIX + "&cFailed to open DATABASE connection. The plugin will disable itself.");
+ if (Storage.MySQL.USE) {
+ PlotSquared.log("$4MYSQL");
+ } else if (Storage.SQLite.USE) {
+ PlotSquared.log("$4SQLITE");
+ }
+ PlotSquared.log(
+ "&d==== Here is an ugly stacktrace, if you are interested in those things ===");
+ e.printStackTrace();
+ PlotSquared.log("&d==== End of stacktrace ====");
+ PlotSquared.log("&6Please go to the " + IMP.getPluginName()
+ + " 'storage.yml' and configure the database correctly.");
+ this.IMP.disable();
+ }
+ }
+
+ /**
+ * Setup the default configuration.
+ *
+ * @throws IOException if the config failed to save
+ */
+ public void setupConfig() throws IOException {
+ String lastVersionString = this.config.getString("version");
+ if (lastVersionString != null) {
+ String[] split = lastVersionString.split("\\.");
+ int[] lastVersion = new int[] {Integer.parseInt(split[0]), Integer.parseInt(split[1]),
+ Integer.parseInt(split[2])};
+ if (checkVersion(new int[] {3, 4, 0}, lastVersion)) {
+ Settings.convertLegacy(configFile);
+ if (config.contains("worlds")) {
+ ConfigurationSection worldSection = config.getConfigurationSection("worlds");
+ worlds.set("worlds", worldSection);
+ try {
+ worlds.save(worldsFile);
+ } catch (IOException e) {
+ PlotSquared.debug("Failed to save " + IMP.getPluginName() + " worlds.yml");
+ e.printStackTrace();
+ }
+ }
+ Settings.save(configFile);
+ }
+ }
+ Settings.load(configFile);
+ try {
+ InputStream stream = getClass().getResourceAsStream("/plugin.properties");
+ BufferedReader br = new BufferedReader(new InputStreamReader(stream));
+ //java.util.Scanner scanner = new java.util.Scanner(stream).useDelimiter("\\A");
+ String versionString = br.readLine();
+ String commitString = br.readLine();
+ String dateString = br.readLine();
+ //scanner.close();
+ br.close();
+ this.version = PlotVersion.tryParse(versionString, commitString, dateString);
+ Settings.DATE = new Date(100 + version.year, version.month, version.day).toGMTString();
+ Settings.BUILD = "https://ci.athion.net/job/PlotSquared/" + version.build;
+ Settings.COMMIT = "https://github.com/IntellectualSites/PlotSquared/commit/" + Integer
+ .toHexString(version.hash);
+ System.out.println("Version is " + this.version);
+ } catch (IOException exception) {
+ exception.printStackTrace();
+ }
+ Settings.save(configFile);
+ config = YamlConfiguration.loadConfiguration(configFile);
+ }
+
+ /**
+ * Setup all configuration files - Config: settings.yml - Storage: storage.yml -
+ * Translation: PlotSquared.use_THIS.yml, style.yml
+ */
+ public boolean setupConfigs() {
+ File folder = new File(this.IMP.getDirectory(), "config");
+ if (!folder.exists() && !folder.mkdirs()) {
+ PlotSquared.log(C.PREFIX
+ + "&cFailed to create the /plugins/config folder. Please create it manually.");
+ }
+ try {
+ this.worldsFile = new File(folder, "worlds.yml");
+ if (!this.worldsFile.exists() && !this.worldsFile.createNewFile()) {
+ PlotSquared.log(
+ "Could not create the worlds file, please create \"worlds.yml\" manually.");
+ }
+ this.worlds = YamlConfiguration.loadConfiguration(this.worldsFile);
+
+ if (this.worlds.contains("worlds")) {
+ if (!this.worlds.contains("configuration_version") || !this.worlds
+ .getString("configuration_version")
+ .equalsIgnoreCase(LegacyConverter.CONFIGURATION_VERSION)) {
+ // Conversion needed
+ log(C.LEGACY_CONFIG_FOUND.s());
+ try {
+ com.google.common.io.Files
+ .copy(this.worldsFile, new File(folder, "worlds.yml.old"));
+ log(C.LEGACY_CONFIG_BACKUP.s());
+ final ConfigurationSection worlds =
+ this.worlds.getConfigurationSection("worlds");
+ final LegacyConverter converter = new LegacyConverter(worlds);
+ converter.convert();
+ this.worlds
+ .set("configuration_version", LegacyConverter.CONFIGURATION_VERSION);
+ this.worlds.set("worlds", worlds); // Redundant, but hey... ¯\_(ツ)_/¯
+ this.worlds.save(this.worldsFile);
+ log(C.LEGACY_CONFIG_DONE.s());
+ } catch (final Exception e) {
+ log(C.LEGACY_CONFIG_CONVERSION_FAILED.s());
+ e.printStackTrace();
+ }
+ // Disable plugin
+ this.IMP.shutdown();
+ return false;
+ }
+ } else {
+ this.worlds.set("configuration_version", LegacyConverter.CONFIGURATION_VERSION);
+ }
+ } catch (IOException ignored) {
+ PlotSquared.log("Failed to save settings.yml");
+ }
+ try {
+ this.configFile = new File(folder, "settings.yml");
+ if (!this.configFile.exists() && !this.configFile.createNewFile()) {
+ PlotSquared.log(
+ "Could not create the settings file, please create \"settings.yml\" manually.");
+ }
+ this.config = YamlConfiguration.loadConfiguration(this.configFile);
+ setupConfig();
+ } catch (IOException ignored) {
+ PlotSquared.log("Failed to save settings.yml");
+ }
+ try {
+ this.styleFile = MainUtil.getFile(IMP.getDirectory(),
+ Settings.Paths.TRANSLATIONS + File.separator + "style.yml");
+ if (!this.styleFile.exists()) {
+ if (!this.styleFile.getParentFile().exists()) {
+ this.styleFile.getParentFile().mkdirs();
+ }
+ if (!this.styleFile.createNewFile()) {
+ PlotSquared.log(
+ "Could not create the style file, please create \"translations/style.yml\" manually");
+ }
+ }
+ this.style = YamlConfiguration.loadConfiguration(this.styleFile);
+ setupStyle();
+ } catch (IOException err) {
+ err.printStackTrace();
+ PlotSquared.log("failed to save style.yml");
+ }
+ try {
+ this.storageFile = new File(folder, "storage.yml");
+ if (!this.storageFile.exists() && !this.storageFile.createNewFile()) {
+ PlotSquared.log(
+ "Could not the storage settings file, please create \"storage.yml\" manually.");
+ }
+ this.storage = YamlConfiguration.loadConfiguration(this.storageFile);
+ setupStorage();
+ } catch (IOException ignored) {
+ PlotSquared.log("Failed to save storage.yml");
+ }
+ try {
+ this.commandsFile = new File(folder, "commands.yml");
+ if (!this.commandsFile.exists() && !this.commandsFile.createNewFile()) {
+ PlotSquared.log(
+ "Could not the storage settings file, please create \"commands.yml\" manually.");
+ }
+ this.commands = YamlConfiguration.loadConfiguration(this.commandsFile);
+ } catch (IOException ignored) {
+ PlotSquared.log("Failed to save commands.yml");
+ }
+ try {
+ this.style.save(this.styleFile);
+ this.commands.save(this.commandsFile);
+ } catch (IOException e) {
+ PlotSquared.log("Configuration file saving failed");
+ e.printStackTrace();
+ }
+ return true;
+ }
+
+ /**
+ * Setup the storage file (load + save missing nodes).
+ */
+ private void setupStorage() {
+ Storage.load(storageFile);
+ Storage.save(storageFile);
+ storage = YamlConfiguration.loadConfiguration(storageFile);
+ }
+
+ /**
+ * Show startup debug information.
+ */
+ private void showDebug() {
+ if (Settings.DEBUG) {
+ Map components = Settings.getFields(Settings.Enabled_Components.class);
+ for (Entry component : components.entrySet()) {
+ PlotSquared.log(C.PREFIX + String
+ .format("&cKey: &6%s&c, Value: &6%s", component.getKey(),
+ component.getValue()));
+ }
+ }
+ }
+
+ /**
+ * Setup the style.yml file
+ */
+ private void setupStyle() {
+ if (this.version != null) {
+ this.style.set("version", this.version.toString());
+ }
+ Map o = new HashMap<>(4);
+ o.put("color.1", "6");
+ o.put("color.2", "7");
+ o.put("color.3", "8");
+ o.put("color.4", "3");
+ if (!this.style.contains("color")) {
+ for (Entry node : o.entrySet()) {
+ this.style.set(node.getKey(), node.getValue());
+ }
+ }
+ }
+
+ /**
+ * Get the Java version.
+ *
+ * @return the java version
+ */
+ public double getJavaVersion() {
+ return Double.parseDouble(System.getProperty("java.specification.version"));
+ }
+
+ public void foreachPlotArea(@NonNull final RunnableVal runnable) {
+ for (final PlotArea area : this.plotAreaManager.getAllPlotAreas()) {
+ runnable.run(area);
+ }
+ }
+
+ public void foreachPlotArea(@NonNull final String world,
+ @NonNull final RunnableVal runnable) {
+ final PlotArea[] array = this.plotAreaManager.getPlotAreas(world, null);
+ if (array == null) {
+ return;
+ }
+ for (final PlotArea area : array) {
+ runnable.run(area);
+ }
+ }
+
+ public void foreachPlot(@NonNull final RunnableVal runnable) {
+ for (final PlotArea area : this.plotAreaManager.getAllPlotAreas()) {
+ area.getPlots().forEach(runnable::run);
+ }
+ }
+
+ public void foreachPlotRaw(@NonNull final RunnableVal runnable) {
+ for (final PlotArea area : this.plotAreaManager.getAllPlotAreas()) {
+ area.getPlots().forEach(runnable::run);
+ }
+ if (this.plots_tmp != null) {
+ for (final HashMap entry : this.plots_tmp.values()) {
+ entry.values().forEach(runnable::run);
+ }
+ }
+ }
+
+ public void foreachBasePlot(@NonNull final RunnableVal run) {
+ for (final PlotArea area : this.plotAreaManager.getAllPlotAreas()) {
+ area.foreachBasePlot(run);
+ }
+ }
+
+ public PlotArea getFirstPlotArea() {
+ PlotArea[] areas = plotAreaManager.getAllPlotAreas();
+ return areas.length > 0 ? areas[0] : null;
+ }
+
+ public int getPlotAreaCount() {
+ return this.plotAreaManager.getAllPlotAreas().length;
+ }
+
+ public int getPlotCount() {
+ int count = 0;
+ for (final PlotArea area : this.plotAreaManager.getAllPlotAreas()) {
+ count += area.getPlotCount();
+ }
+ return count;
+ }
+
+ public Set