diff --git a/src/main/java/fr/xephi/authme/permission/PlayerPermission.java b/src/main/java/fr/xephi/authme/permission/PlayerPermission.java
index 4ef0aca2e..8ae97407f 100644
--- a/src/main/java/fr/xephi/authme/permission/PlayerPermission.java
+++ b/src/main/java/fr/xephi/authme/permission/PlayerPermission.java
@@ -81,7 +81,7 @@ public enum PlayerPermission implements PermissionNode {
SEE_OTHER_ACCOUNTS("authme.player.seeotheraccounts"),
/**
- *
+ * Permission to use all player (non-admin) commands.
*/
ALL_COMMANDS("authme.player.*");
diff --git a/src/tools/README.md b/src/tools/README.md
new file mode 100644
index 000000000..b61dabead
--- /dev/null
+++ b/src/tools/README.md
@@ -0,0 +1,3 @@
+# About src/tools
+This _tools_ folder provides helpers and extended tests useful during the development of AuthMe.
+This folder is not included during the build of AuthMe.
diff --git a/src/tools/docs/permission_nodes.md b/src/tools/docs/permission_nodes.md
new file mode 100644
index 000000000..8e78dee43
--- /dev/null
+++ b/src/tools/docs/permission_nodes.md
@@ -0,0 +1,43 @@
+
+
+
+## AuthMe Permission Nodes
+The following are the permission nodes that are currently supported by the latest dev builds.
+
+- **authme.admin.*** – Give access to all admin commands.
+- **authme.admin.accounts** – Administrator command to see all accounts associated with a user.
+- **authme.admin.changemail** – Administrator command to set or change the email address of a user.
+- **authme.admin.changepassword** – Administrator command to change the password of a user.
+- **authme.admin.converter** – Administrator command to convert old or other data to AuthMe data.
+- **authme.admin.firstspawn** – Administrator command to teleport to the first AuthMe spawn.
+- **authme.admin.forcelogin** – Administrator command to force-login an existing user.
+- **authme.admin.getemail** – Administrator command to get the email address of a user, if set.
+- **authme.admin.getip** – Administrator command to get the last known IP of a user.
+- **authme.admin.lastlogin** – Administrator command to see the last login date and time of a user.
+- **authme.admin.purge** – Administrator command to purge old user data.
+- **authme.admin.purgebannedplayers** – Administrator command to purge all data associated with banned players.
+- **authme.admin.purgelastpos** – Administrator command to purge the last position of a user.
+- **authme.admin.register** – Administrator command to register a new user.
+- **authme.admin.reload** – Administrator command to reload the plugin configuration.
+- **authme.admin.setfirstspawn** – Administrator command to set the first AuthMe spawn.
+- **authme.admin.setspawn** – Administrator command to set the AuthMe spawn.
+- **authme.admin.spawn** – Administrator command to teleport to the AuthMe spawn.
+- **authme.admin.switchantibot** – Administrator command to toggle the AntiBot protection status.
+- **authme.admin.unregister** – Administrator command to unregister an existing user.
+- **authme.player.*** – Permission to use all player (non-admin) commands.
+- **authme.player.allow2accounts** – Permission for users to allow two accounts.
+- **authme.player.bypassantibot** – Permission node to bypass AntiBot protection.
+- **authme.player.bypassforcesurvival** – Permission for users to bypass force-survival mode.
+- **authme.player.canbeforced** – Permission for users a login can be forced to.
+- **authme.player.captcha** – Command permission to use captcha.
+- **authme.player.changepassword** – Command permission to change the password.
+- **authme.player.email.add** – Command permission to add an email address.
+- **authme.player.email.change** – Command permission to change the email address.
+- **authme.player.email.recover** – Command permission to recover an account using it's email address.
+- **authme.player.login** – Command permission to login.
+- **authme.player.logout** – Command permission to logout.
+- **authme.player.register** – Command permission to register.
+- **authme.player.seeotheraccounts** – Permission for user to see other accounts.
+- **authme.player.unregister** – Command permission to unregister.
+- **authme.player.vip** – Permission node to identify VIP users.
+
diff --git a/src/tools/permissions/PermissionNodesGatherer.java b/src/tools/permissions/PermissionNodesGatherer.java
new file mode 100644
index 000000000..5b61adbef
--- /dev/null
+++ b/src/tools/permissions/PermissionNodesGatherer.java
@@ -0,0 +1,113 @@
+package permissions;
+
+import fr.xephi.authme.permission.AdminPermission;
+import fr.xephi.authme.permission.PermissionNode;
+import fr.xephi.authme.permission.PlayerPermission;
+import fr.xephi.authme.util.StringUtils;
+
+import java.io.IOException;
+import java.nio.charset.Charset;
+import java.nio.file.Files;
+import java.nio.file.Paths;
+import java.util.EnumSet;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.Set;
+import java.util.TreeMap;
+import java.util.TreeSet;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+/**
+ * Gatherer to generate up-to-date lists of the AuthMe permission nodes.
+ */
+public class PermissionNodesGatherer {
+
+ /** The folder in which the implementations of {@link PermissionNode} reside. */
+ private static final String PERMISSION_NODE_SOURCE_FOLDER =
+ "src/main/java/fr/xephi/authme/permission/";
+
+ /**
+ * Regular expression that should match the JavaDoc comment above an enum, including
+ * the name of the enum value. The first group (i.e. {@code \\1}) should be the JavaDoc description;
+ * the second group should contain the enum value.
+ */
+ private static final Pattern JAVADOC_WITH_ENUM_PATTERN = Pattern.compile(
+ "/\\*\\*\\s+\\*" // Match starting '/**' and the '*' on the next line
+ + "(.*?)\\s+\\*/" // Capture everything until we encounter '*/'
+ + "\\s+([A-Z_]+)\\("); // Match the enum name (e.g. 'LOGIN'), until before the first '('
+
+ /**
+ * Return a sorted collection of all permission nodes.
+ *
+ * @return AuthMe permission nodes sorted alphabetically
+ */
+ public Set gatherNodes() {
+ Set nodes = new TreeSet<>();
+ for (PermissionNode perm : PlayerPermission.values()) {
+ nodes.add(perm.getNode());
+ }
+ for (PermissionNode perm : AdminPermission.values()) {
+ nodes.add(perm.getNode());
+ }
+ return nodes;
+ }
+
+ /**
+ * Return a sorted collection of all permission nodes, including its JavaDoc description.
+ *
+ * @return Ordered map whose keys are the permission nodes and the values the associated JavaDoc
+ */
+ public Map gatherNodesWithJavaDoc() {
+ Map result = new TreeMap<>();
+ addDescriptionsForClass(PlayerPermission.class, result);
+ addDescriptionsForClass(AdminPermission.class, result);
+ return result;
+ }
+
+ private & PermissionNode> void addDescriptionsForClass(Class clazz,
+ Map descriptions) {
+ String classSource = getSourceForClass(clazz);
+ Map sourceDescriptions = extractJavaDocFromSource(classSource);
+
+ for (T perm : EnumSet.allOf(clazz)) {
+ String description = sourceDescriptions.get(perm.name());
+ if (description == null) {
+ System.out.println("Note: Could not retrieve description for "
+ + clazz.getSimpleName() + "#" + perm.name());
+ description = "";
+ }
+ descriptions.put(perm.getNode(), description.trim());
+ }
+ }
+
+ private static Map extractJavaDocFromSource(String source) {
+ Map allMatches = new HashMap<>();
+ Matcher matcher = JAVADOC_WITH_ENUM_PATTERN.matcher(source);
+ while (matcher.find()) {
+ String description = matcher.group(1);
+ String enumValue = matcher.group(2);
+ allMatches.put(enumValue, description);
+ }
+ return allMatches;
+ }
+
+ /**
+ * Return the Java source code for the given implementation of {@link PermissionNode}.
+ *
+ * @param clazz The clazz to the get the source for
+ * @param The concrete class
+ * @return Source code of the file
+ */
+ private static & PermissionNode> String getSourceForClass(Class clazz) {
+ String classFile = PERMISSION_NODE_SOURCE_FOLDER + clazz.getSimpleName() + ".java";
+ Charset cs = Charset.forName("utf-8");
+ try {
+ return StringUtils.join("\n",
+ Files.readAllLines(Paths.get(classFile), cs));
+ } catch (IOException e) {
+ throw new RuntimeException("Failed to get the source for class '" + clazz.getSimpleName() + "'");
+ }
+ }
+
+}
diff --git a/src/tools/permissions/PermissionsListWriter.java b/src/tools/permissions/PermissionsListWriter.java
new file mode 100644
index 000000000..7d361b1c5
--- /dev/null
+++ b/src/tools/permissions/PermissionsListWriter.java
@@ -0,0 +1,86 @@
+package permissions;
+
+import utils.ANewMap;
+import utils.GeneratedFileWriter;
+import utils.TagReplacer;
+import utils.ToolsConstants;
+
+import java.util.Map;
+import java.util.Scanner;
+import java.util.Set;
+
+/**
+ * Class responsible for formatting a permissions node list and
+ * for writing it to a file if desired.
+ */
+public class PermissionsListWriter {
+
+ private static final String PERMISSIONS_OUTPUT_FILE = ToolsConstants.DOCS_FOLDER + "permission_nodes.md";
+
+ public static void main(String[] args) {
+ // Ask if result should be written to file
+ Scanner scanner = new Scanner(System.in);
+ System.out.println("Include description? [Enter 'n' for no]");
+ boolean includeDescription = !matches("n", scanner);
+
+ if (!includeDescription) {
+ outputSimpleList();
+ return;
+ }
+
+ System.out.println("Write to file? [Enter 'n' for console output]");
+ boolean writeToFile = !matches("n", scanner);
+ scanner.close();
+
+
+ if (writeToFile) {
+ generateAndWriteFile();
+ } else {
+ System.out.println(generatePermissionsList());
+ }
+ }
+
+
+ private static void generateAndWriteFile() {
+ final String permissionsTagValue = generatePermissionsList();
+
+ Map tags = ANewMap.with("permissions", permissionsTagValue).build();
+ GeneratedFileWriter.generateFileFromTemplate(
+ ToolsConstants.TOOLS_SOURCE_ROOT + "permissions/permission_nodes.tpl.md", PERMISSIONS_OUTPUT_FILE, tags);
+ System.out.println("Wrote to '" + PERMISSIONS_OUTPUT_FILE + "'");
+ System.out.println("Before committing, please verify the output!");
+ }
+
+ private static String generatePermissionsList() {
+ PermissionNodesGatherer gatherer = new PermissionNodesGatherer();
+ Map permissions = gatherer.gatherNodesWithJavaDoc();
+
+ final String template = GeneratedFileWriter.readFromToolsFile("permissions/permission_node_entry.tpl.md");
+ StringBuilder sb = new StringBuilder();
+
+ for (Map.Entry entry : permissions.entrySet()) {
+ Map tags = ANewMap.
+ with("node", entry.getKey())
+ .and("description", entry.getValue())
+ .build();
+ sb.append(TagReplacer.applyReplacements(template, tags));
+ }
+ return sb.toString();
+ }
+
+ private static void outputSimpleList() {
+ PermissionNodesGatherer gatherer = new PermissionNodesGatherer();
+ Set nodes = gatherer.gatherNodes();
+ for (String node : nodes) {
+ System.out.println(node);
+ }
+ System.out.println();
+ System.out.println("Total: " + nodes.size());
+ }
+
+ private static boolean matches(String answer, Scanner sc) {
+ String userInput = sc.nextLine();
+ return answer.equalsIgnoreCase(userInput);
+ }
+
+}
diff --git a/src/tools/permissions/README.md b/src/tools/permissions/README.md
new file mode 100644
index 000000000..9603cd386
--- /dev/null
+++ b/src/tools/permissions/README.md
@@ -0,0 +1,2 @@
+# About
+Helper script to generate a page with an up-to-date list of permission nodes.
diff --git a/src/tools/permissions/permission_node_entry.tpl.md b/src/tools/permissions/permission_node_entry.tpl.md
new file mode 100644
index 000000000..ab7f06872
--- /dev/null
+++ b/src/tools/permissions/permission_node_entry.tpl.md
@@ -0,0 +1 @@
+- **{node}** – {description}
diff --git a/src/tools/permissions/permission_nodes.tpl.md b/src/tools/permissions/permission_nodes.tpl.md
new file mode 100644
index 000000000..641d28dfa
--- /dev/null
+++ b/src/tools/permissions/permission_nodes.tpl.md
@@ -0,0 +1,7 @@
+
+
+
+## AuthMe Permission Nodes
+The following are the permission nodes that are currently supported by the latest dev builds.
+
+{permissions}
diff --git a/src/tools/utils/ANewMap.java b/src/tools/utils/ANewMap.java
new file mode 100644
index 000000000..de37bd746
--- /dev/null
+++ b/src/tools/utils/ANewMap.java
@@ -0,0 +1,36 @@
+package utils;
+
+import java.util.HashMap;
+import java.util.Map;
+
+/**
+ * A map builder for the lazy.
+ *
+ * Sample usage:
+ *
+ * Map<String, Integer> map = ANewMap
+ * .with("test", 123)
+ * .and("text", 938)
+ * .and("abc", 456)
+ * .build();
+ *
+ */
+public class ANewMap {
+
+ private Map map = new HashMap<>();
+
+ public static ANewMap with(K key, V value) {
+ ANewMap instance = new ANewMap<>();
+ return instance.and(key, value);
+ }
+
+ public ANewMap and(K key, V value) {
+ map.put(key, value);
+ return this;
+ }
+
+ public Map build() {
+ return map;
+ }
+
+}
diff --git a/src/tools/utils/GeneratedFileWriter.java b/src/tools/utils/GeneratedFileWriter.java
new file mode 100644
index 000000000..06cbcaf3d
--- /dev/null
+++ b/src/tools/utils/GeneratedFileWriter.java
@@ -0,0 +1,47 @@
+package utils;
+
+import java.io.IOException;
+import java.nio.charset.Charset;
+import java.nio.file.Files;
+import java.nio.file.Paths;
+import java.util.Map;
+
+/**
+ * Utility class for writing a generated file with a timestamp.
+ */
+public final class GeneratedFileWriter {
+
+ private final static Charset CHARSET = Charset.forName("utf-8");
+
+ private GeneratedFileWriter() {
+ }
+
+ public static void generateFileFromTemplate(String templateFile, String destinationFile, Map tags) {
+ String template = readFromFile(templateFile);
+ String result = TagReplacer.applyReplacements(template, tags);
+
+ writeToFile(destinationFile, result);
+ }
+
+ private static void writeToFile(String outputFile, String contents) {
+ try {
+ Files.write(Paths.get(outputFile), contents.getBytes());
+ } catch (IOException e) {
+ throw new RuntimeException("Failed to write to file '" + outputFile + "'", e);
+ }
+ }
+
+ public static String readFromFile(String file) {
+ try {
+ return new String(Files.readAllBytes(Paths.get(file)), CHARSET);
+ } catch (IOException e) {
+ throw new RuntimeException("Could not read from file '" + file + "'", e);
+ }
+ }
+
+ public static String readFromToolsFile(String file) {
+ return readFromFile(ToolsConstants.TOOLS_SOURCE_ROOT + file);
+ }
+
+
+}
diff --git a/src/tools/utils/TagReplacer.java b/src/tools/utils/TagReplacer.java
new file mode 100644
index 000000000..404857b7f
--- /dev/null
+++ b/src/tools/utils/TagReplacer.java
@@ -0,0 +1,46 @@
+package utils;
+
+import java.util.Date;
+import java.util.Map;
+
+/**
+ * Class responsible for replacing template tags to actual content.
+ * For all files, the following tags are defined:
+ *
+ * - {gen_date} – the generation date
+ * - {gen_warning} - warning not to edit the generated file directly
+ *
+ */
+public class TagReplacer {
+
+ /**
+ * Replace a template with default tags and custom ones supplied by a map.
+ *
+ * @param template The template to process
+ * @param tags Map with additional tags, e.g. a map entry with key "foo" and value "bar" will replace
+ * any occurrences of "{foo}" to "bar".
+ * @return The filled template
+ */
+ public static String applyReplacements(String template, Map tags) {
+ String result = template;
+ for (Map.Entry tagRule : tags.entrySet()) {
+ result = result.replace("{" + tagRule.getKey() + "}", tagRule.getValue().toString());
+ }
+
+ return applyReplacements(result);
+ }
+
+ /**
+ * Apply the default tag replacements.
+ *
+ * @param template The template to process
+ * @return The filled template
+ */
+ public static String applyReplacements(String template) {
+ return template
+ .replace("{gen_date}", new Date().toString())
+ .replace("{gen_warning}", "AUTO-GENERATED FILE! Do not edit this directly");
+ }
+
+
+}
diff --git a/src/tools/utils/ToolsConstants.java b/src/tools/utils/ToolsConstants.java
new file mode 100644
index 000000000..30cb0f413
--- /dev/null
+++ b/src/tools/utils/ToolsConstants.java
@@ -0,0 +1,17 @@
+package utils;
+
+/**
+ * Constants for the src/tools folder.
+ */
+public final class ToolsConstants {
+
+ private ToolsConstants() {
+ }
+
+ public static final String MAIN_SOURCE_ROOT = "src/main/java/";
+
+ public static final String TOOLS_SOURCE_ROOT = "src/tools/";
+
+ public static final String DOCS_FOLDER = TOOLS_SOURCE_ROOT + "docs/";
+
+}