Move tools folder into test folder

- Classes still won't be present in JAR but classes will be automatically compiled by Maven inside of the test scope, facilitating the execution of tool tasks
This commit is contained in:
ljacqu
2016-05-03 20:24:34 +02:00
parent 23317caa46
commit 3645806edc
46 changed files with 69 additions and 64 deletions
+6
View File
@@ -0,0 +1,6 @@
# About the _tools_ Folder
This _tools_ folder provides helpers and extended tests useful during the development of AuthMe.
This folder is not included during the build of AuthMe and does not contain unit tests.
Run the `ToolsRunner` class to perform a task.
+130
View File
@@ -0,0 +1,130 @@
package tools;
import tools.utils.ToolTask;
import tools.utils.ToolsConstants;
import java.io.File;
import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Modifier;
import java.util.Map;
import java.util.Scanner;
import java.util.TreeMap;
/**
* Runner for executing tool tasks.
*/
public final class ToolsRunner {
private ToolsRunner() {
}
/**
* Entry point of the runner.
*
* @param args .
*/
public static void main(String... args) {
// Collect tasks and show them
File toolsFolder = new File(ToolsConstants.TOOLS_SOURCE_ROOT);
Map<String, ToolTask> tasks = new TreeMap<>(String.CASE_INSENSITIVE_ORDER);
collectTasksInDirectory(toolsFolder, tasks);
listAllTasks(tasks);
// Prompt user for task and handle input
System.out.println("Please enter the task to run:");
Scanner scanner = new Scanner(System.in);
String inputTask = scanner.nextLine();
ToolTask task = tasks.get(inputTask);
if (task != null) {
task.execute(scanner);
} else {
System.out.println("Unknown task");
}
scanner.close();
}
private static void listAllTasks(Map<String, ToolTask> taskCollection) {
System.out.println("The following tasks are available:");
for (String key : taskCollection.keySet()) {
System.out.println("- " + key);
}
}
/**
* Add all implementations of {@link ToolTask} from the given folder to the provided collection.
*
* @param dir The directory to scan
* @param taskCollection The collection to add results to
*/
// Note ljacqu 20151212: If the tools folder becomes a lot bigger, it will make sense to restrict the depth
// of this recursive collector
private static void collectTasksInDirectory(File dir, Map<String, ToolTask> taskCollection) {
File[] files = dir.listFiles();
if (files == null) {
throw new RuntimeException("Cannot read folder '" + dir + "'");
}
for (File file : files) {
if (file.isDirectory()) {
collectTasksInDirectory(file, taskCollection);
} else if (file.isFile()) {
ToolTask task = getTaskFromFile(file);
if (task != null) {
taskCollection.put(task.getTaskName(), task);
}
}
}
}
/**
* Return a {@link ToolTask} instance defined by the given source file.
*
* @param file The file to load
* @return ToolTask instance, or null if not applicable
*/
private static ToolTask getTaskFromFile(File file) {
Class<? extends ToolTask> taskClass = loadTaskClassFromFile(file);
if (taskClass == null) {
return null;
}
try {
Constructor<? extends ToolTask> constructor = taskClass.getConstructor();
return constructor.newInstance();
} catch (NoSuchMethodException | InvocationTargetException |
IllegalAccessException | InstantiationException e) {
throw new RuntimeException("Cannot instantiate task '" + taskClass + "'");
}
}
/**
* Return the class the file defines if it implements {@link ToolTask}.
*
* @return The class instance, or null if not applicable
*/
@SuppressWarnings("unchecked")
private static Class<? extends ToolTask> loadTaskClassFromFile(File file) {
if (!file.getName().endsWith(".java")) {
return null;
}
String filePath = file.getPath();
String className = "tools." + filePath
.substring(ToolsConstants.TOOLS_SOURCE_ROOT.length(), filePath.length() - 5)
.replace(File.separator, ".");
try {
Class<?> clazz = ClassLoader.getSystemClassLoader().loadClass(className);
return ToolTask.class.isAssignableFrom(clazz) && isInstantiable(clazz)
? (Class<? extends ToolTask>) clazz
: null;
} catch (ClassNotFoundException e) {
throw new RuntimeException(e);
}
}
private static boolean isInstantiable(Class<?> clazz) {
return !clazz.isInterface() && !Modifier.isAbstract(clazz.getModifiers());
}
}
+21
View File
@@ -0,0 +1,21 @@
## Bat Helpers
Collection of .bat files to quickly perform some frequent development tasks.
They allow you to quickly build the project and to move the generated JAR to
the plugins folder of your test server.
### Setup
1. Copy the files into a new, convenient directory
2. Open setvars.bat with a text editor and add the correct directories
3. Open `cmd` and navigate to your _bathelpers_ folder (`cd C:\path\the\folder`)
4. Type `list_files.bat` (Hint: Type `l` and hit Tab) to see the available tasks
### Example use case
1. After writing changes, `build_project` to build project
2. `move_plugin` moves the JAR file to the plugin folder
3. `run_server` to start the server with the fresh JAR
4. Problem detected, stop the server
5. Make a small change, use `quick_build` and `move_plugin` to update
6. Verify the change again on the server: `run_server`
All files start with a different letter, so you can conveniently type the
first letter and then complete with Tab.
@@ -0,0 +1,6 @@
: Analyze the project with Sonar (requires you install SonarQube)
if "%jarfile%" == "" (
call setvars.bat
)
mvn clean verify sonar:sonar -f "%pomfile%"
@@ -0,0 +1,6 @@
: Build the project normally
if "%jarfile%" == "" (
call setvars.bat
)
mvn clean install -f "%pomfile%" -B
@@ -0,0 +1,2 @@
: List all bat files in the directory
dir /B *.bat
@@ -0,0 +1,11 @@
: Moves the AuthMe JAR file to the plugins folder of the test server
: You will have to hit 'Y' to really replace it if it already exists
if "%jarfile%" == "" (
call setvars.bat
)
if exist %jarfile% (
xcopy %jarfile% %plugins%
) else (
echo Target file not found: '%jarfile%'
)
@@ -0,0 +1,6 @@
: Build quickly without cleaning or testing
if "%jarfile%" == "" (
call setvars.bat
)
mvn install -f "%pomfile%" -Dmaven.test.skip
@@ -0,0 +1,9 @@
: Start the Minecraft server
if "%jarfile%" == "" (
call setvars.bat
)
cd "%server%"
call java -Xmx1024M -Xms1024M -jar spigot_server.jar
cd "%batdir%"
dir /B *.bat
@@ -0,0 +1,14 @@
: The folder in which these .bat files are located
SET batdir=C:\your\path\AUTHME_DEV\bathelpers\
: The location of the generated JAR file
SET jarfile=C:\Users\yourname\IdeaProjects\AuthMeReloaded\target\AuthMe-5.2-SNAPSHOT.jar
: The location of the pom.xml file of the project
SET pomfile=C:\Users\yourname\IdeaProjects\AuthMeReloaded\pom.xml
: The folder in which the server is located
SET server=C:\your\path\AUTHME_DEV\spigot-server\
: The location of the plugins folder of the Minecraft server
SET plugins=%server%\plugins
@@ -0,0 +1,78 @@
package tools.commands;
import fr.xephi.authme.command.CommandArgumentDescription;
import fr.xephi.authme.command.CommandDescription;
import fr.xephi.authme.command.CommandPermissions;
import fr.xephi.authme.command.CommandUtils;
import fr.xephi.authme.permission.PermissionNode;
import tools.utils.FileUtils;
import tools.utils.TagValue.NestedTagValue;
import tools.utils.TagValueHolder;
import tools.utils.ToolTask;
import tools.utils.ToolsConstants;
import java.util.Collection;
import java.util.HashSet;
import java.util.Scanner;
import java.util.Set;
public class CommandPageCreater implements ToolTask {
private static final String OUTPUT_FILE = ToolsConstants.DOCS_FOLDER + "commands.md";
@Override
public String getTaskName() {
return "createCommandPage";
}
@Override
public void execute(Scanner scanner) {
// TODO ljacqu 20160427: Fix initialization of commands
final Set<CommandDescription> baseCommands = new HashSet<>();//CommandInitializer.buildCommands();
NestedTagValue commandTags = new NestedTagValue();
addCommandsInfo(commandTags, baseCommands);
FileUtils.generateFileFromTemplate(
ToolsConstants.TOOLS_SOURCE_ROOT + "commands/commands.tpl.md",
OUTPUT_FILE,
TagValueHolder.create().put("commands", commandTags));
System.out.println("Wrote to '" + OUTPUT_FILE + "' with " + baseCommands.size() + " base commands.");
}
private static void addCommandsInfo(NestedTagValue commandTags, Collection<CommandDescription> commands) {
for (CommandDescription command : commands) {
TagValueHolder tags = TagValueHolder.create()
.put("command", CommandUtils.constructCommandPath(command))
.put("description", command.getDetailedDescription())
.put("arguments", formatArguments(command.getArguments()))
.put("permissions", formatPermissions(command.getCommandPermissions()));
commandTags.add(tags);
if (!command.getChildren().isEmpty()) {
addCommandsInfo(commandTags, command.getChildren());
}
}
}
private static String formatPermissions(CommandPermissions permissions) {
if (permissions == null) {
return "";
}
String result = "";
for (PermissionNode node : permissions.getPermissionNodes()) {
result += node.getNode() + " ";
}
return result.trim();
}
private static String formatArguments(Iterable<CommandArgumentDescription> arguments) {
StringBuilder result = new StringBuilder();
for (CommandArgumentDescription argument : arguments) {
String argumentName = argument.isOptional()
? "[" + argument.getName() + "]"
: "&lt;" + argument.getName() + ">";
result.append(" ").append(argumentName);
}
return result.toString();
}
}
@@ -0,0 +1,13 @@
<!-- {gen_warning} -->
<!-- File auto-generated on {gen_date}. See commands/commands.tpl.md -->
## AuthMe Commands
You can use the following commands to use the features of AuthMe. Mandatory arguments are marked with `< >`
brackets; optional arguments are enclosed in square brackets (`[ ]`).
[#commands]
- **{command}**{arguments}: {description}[permissions]
<br />Requires `{permissions}`[/permissions]
[/#commands]
{gen_footer}
@@ -0,0 +1,46 @@
package tools.docs;
import com.google.common.collect.ImmutableSet;
import tools.commands.CommandPageCreater;
import tools.hashmethods.HashAlgorithmsDescriptionTask;
import tools.permissions.PermissionsListWriter;
import tools.utils.ToolTask;
import java.util.Scanner;
import java.util.Set;
/**
* Task that runs all tasks which update files in the docs folder.
*/
public class UpdateDocsTask implements ToolTask {
private final Set<Class<? extends ToolTask>> TASKS = ImmutableSet.of(
CommandPageCreater.class, HashAlgorithmsDescriptionTask.class, PermissionsListWriter.class);
@Override
public String getTaskName() {
return "updateDocs";
}
@Override
public void execute(Scanner scanner) {
for (Class<? extends ToolTask> taskClass : TASKS) {
try {
ToolTask task = instantiateTask(taskClass);
System.out.println("\nRunning " + task.getTaskName() + "\n-------------------");
task.execute(scanner);
} catch (UnsupportedOperationException e) {
System.err.println("Error running task of class '" + taskClass + "'");
e.printStackTrace();
}
}
}
private static ToolTask instantiateTask(Class<? extends ToolTask> clazz) {
try {
return clazz.newInstance();
} catch (IllegalAccessException | InstantiationException e) {
throw new UnsupportedOperationException(e);
}
}
}
@@ -0,0 +1,152 @@
package tools.hashmethods;
import fr.xephi.authme.security.HashAlgorithm;
import fr.xephi.authme.security.crypts.EncryptionMethod;
import fr.xephi.authme.security.crypts.HexSaltedMethod;
import fr.xephi.authme.security.crypts.description.AsciiRestricted;
import fr.xephi.authme.security.crypts.description.HasSalt;
import fr.xephi.authme.security.crypts.description.Recommendation;
import fr.xephi.authme.settings.NewSetting;
import fr.xephi.authme.settings.properties.HooksSettings;
import fr.xephi.authme.settings.properties.SecuritySettings;
import org.mockito.BDDMockito;
import java.lang.annotation.Annotation;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.Set;
import static com.google.common.collect.Sets.newHashSet;
import static org.mockito.Mockito.mock;
/**
* Gathers information on {@link EncryptionMethod} implementations based on
* the annotations in {@link fr.xephi.authme.security.crypts.description}.
*/
public class EncryptionMethodInfoGatherer {
@SuppressWarnings("unchecked")
private final static Set<Class<? extends Annotation>> RELEVANT_ANNOTATIONS =
newHashSet(HasSalt.class, Recommendation.class, AsciiRestricted.class);
private static NewSetting settings = createSettings();
private Map<HashAlgorithm, MethodDescription> descriptions;
public EncryptionMethodInfoGatherer() {
descriptions = new LinkedHashMap<>();
constructDescriptions();
}
public Map<HashAlgorithm, MethodDescription> getDescriptions() {
return descriptions;
}
private void constructDescriptions() {
for (HashAlgorithm algorithm : HashAlgorithm.values()) {
if (!HashAlgorithm.CUSTOM.equals(algorithm) && !algorithm.getClazz().isAnnotationPresent(Deprecated.class)) {
MethodDescription description = createDescription(algorithm);
descriptions.put(algorithm, description);
}
}
}
private static MethodDescription createDescription(HashAlgorithm algorithm) {
Class<? extends EncryptionMethod> clazz = algorithm.getClazz();
EncryptionMethod method = null; // TODO ljacqu PasswordSecurity.initializeEncryptionMethod(algorithm, settings);
if (method == null) {
throw new NullPointerException("Method for '" + algorithm + "' is null");
}
MethodDescription description = new MethodDescription(clazz);
description.setHashLength(method.computeHash("test", "user").getHash().length());
description.setHasSeparateSalt(method.hasSeparateSalt());
Map<Class<?>, Annotation> annotationMap = gatherAnnotations(clazz);
if (annotationMap.containsKey(HasSalt.class)) {
setSaltInformation(description, returnTyped(annotationMap, HasSalt.class), method);
}
if (annotationMap.containsKey(Recommendation.class)) {
description.setUsage(returnTyped(annotationMap, Recommendation.class).value());
}
if (annotationMap.containsKey(AsciiRestricted.class)) {
description.setAsciiRestricted(true);
}
return description;
}
private static Map<Class<?>, Annotation> gatherAnnotations(Class<?> methodClass) {
// Note ljacqu 20151231: The map could be Map<Class<? extends Annotation>, Annotation> and it has the constraint
// that for a key Class<T>, the value is of type T. We write a simple "Class<?>" for brevity.
Map<Class<?>, Annotation> collection = new HashMap<>();
Class<?> currentMethodClass = methodClass;
while (currentMethodClass != null) {
getRelevantAnnotations(currentMethodClass, collection);
currentMethodClass = getSuperClass(currentMethodClass);
}
return collection;
}
// Parameters could be Class<? extends EncryptionMethod>; Map<Class<? extends Annotation>, Annotation>
// but the constraint doesn't have any technical relevance, so just clutters the code
private static void getRelevantAnnotations(Class<?> methodClass, Map<Class<?>, Annotation> collection) {
for (Annotation annotation : methodClass.getAnnotations()) {
if (RELEVANT_ANNOTATIONS.contains(annotation.annotationType())
&& !collection.containsKey(annotation.annotationType())) {
collection.put(annotation.annotationType(), annotation);
}
}
}
/**
* Returns the super class of the given encryption method if it is also of EncryptionMethod type.
* (Anything beyond EncryptionMethod is not of interest.)
*/
private static Class<?> getSuperClass(Class<?> methodClass) {
Class<?> zuper = methodClass.getSuperclass();
if (EncryptionMethod.class.isAssignableFrom(zuper)) {
return zuper;
}
return null;
}
/**
* Set the salt information for the given encryption method and the found {@link HasSalt} annotation.
* Also gets the salt length from {@link HexSaltedMethod#getSaltLength()} for such instances.
*
* @param description The description to update
* @param hasSalt The associated HasSalt annotation
* @param method The encryption method
*/
private static void setSaltInformation(MethodDescription description, HasSalt hasSalt, EncryptionMethod method) {
description.setSaltType(hasSalt.value());
if (hasSalt.length() != 0) {
description.setSaltLength(hasSalt.length());
} else if (method instanceof HexSaltedMethod) {
int saltLength = ((HexSaltedMethod) method).getSaltLength();
description.setSaltLength(saltLength);
}
}
// Convenience method for retrieving an annotation in a typed fashion.
// We know implicitly that the key of the map always corresponds to the type of the value
private static <T> T returnTyped(Map<Class<?>, Annotation> map, Class<T> key) {
return key.cast(map.get(key));
}
private static NewSetting createSettings() {
// TODO #672 Don't mock settings but instantiate a NewSetting object without any validation / migration
NewSetting settings = mock(NewSetting.class);
BDDMockito.given(settings.getProperty(HooksSettings.BCRYPT_LOG2_ROUND)).willReturn(8);
BDDMockito.given(settings.getProperty(SecuritySettings.DOUBLE_MD5_SALT_LENGTH)).willReturn(8);
return settings;
/*try (InputStreamReader isr = new InputStreamReader(getClass().getResourceAsStream("config.yml"))) {
FileConfiguration configuration = YamlConfiguration.loadConfiguration(isr);
return new NewSetting(configuration, null, null, null);
} catch (IOException e) {
throw new UnsupportedOperationException(e);
}*/
}
}
@@ -0,0 +1,90 @@
package tools.hashmethods;
import fr.xephi.authme.security.HashAlgorithm;
import tools.utils.FileUtils;
import tools.utils.TagValue.NestedTagValue;
import tools.utils.TagValueHolder;
import tools.utils.ToolTask;
import tools.utils.ToolsConstants;
import java.util.Map;
import java.util.Scanner;
/**
* Task for generating the markdown page describing the AuthMe hash algorithms.
*
* @see {@link fr.xephi.authme.security.HashAlgorithm}
*/
public class HashAlgorithmsDescriptionTask implements ToolTask {
private static final String CUR_FOLDER = ToolsConstants.TOOLS_SOURCE_ROOT + "hashmethods/";
private static final String OUTPUT_FILE = ToolsConstants.DOCS_FOLDER + "hash_algorithms.md";
@Override
public void execute(Scanner scanner) {
// Gather info and construct a row for each method
EncryptionMethodInfoGatherer infoGatherer = new EncryptionMethodInfoGatherer();
Map<HashAlgorithm, MethodDescription> descriptions = infoGatherer.getDescriptions();
final NestedTagValue methodRows = constructMethodRows(descriptions);
// Write to the docs file
TagValueHolder tags = TagValueHolder.create().put("algorithms", methodRows);
FileUtils.generateFileFromTemplate(CUR_FOLDER + "hash_algorithms.tpl.md", OUTPUT_FILE, tags);
}
private static NestedTagValue constructMethodRows(Map<HashAlgorithm, MethodDescription> descriptions) {
NestedTagValue methodTags = new NestedTagValue();
for (Map.Entry<HashAlgorithm, MethodDescription> entry : descriptions.entrySet()) {
MethodDescription description = entry.getValue();
TagValueHolder tags = TagValueHolder.create()
.put("name", asString(entry.getKey()))
.put("recommendation", asString(description.getUsage()))
.put("hash_length", asString(description.getHashLength()))
.put("ascii_restricted", asString(description.isAsciiRestricted()))
.put("salt_type", asString(description.getSaltType()))
.put("salt_length", asString(description.getSaltLength()))
.put("separate_salt", asString(description.hasSeparateSalt()));
methodTags.add(tags);
}
return methodTags;
}
@Override
public String getTaskName() {
return "describeHashAlgos";
}
// ----
// String representations
// ----
private static String asString(boolean value) {
return value ? "Y" : "";
}
private static String asString(int value) {
return String.valueOf(value);
}
private static String asString(Integer value) {
if (value == null) {
return "";
}
return String.valueOf(value);
}
private static String asString(HashAlgorithm value) {
return value.toString();
}
private static <E extends Enum<E>> String asString(E value) {
if (value == null) {
return "";
}
// Get the enum name and replace something like "DO_NOT_USE" to "Do not use"
String enumName = value.toString().replace("_", " ");
return enumName.length() > 2
? enumName.substring(0, 1) + enumName.substring(1).toLowerCase()
: enumName;
}
}
@@ -0,0 +1,85 @@
package tools.hashmethods;
import fr.xephi.authme.security.crypts.EncryptionMethod;
import fr.xephi.authme.security.crypts.description.SaltType;
import fr.xephi.authme.security.crypts.description.Usage;
/**
* Description of a {@link EncryptionMethod}.
*/
public class MethodDescription {
/** The implementation class the description belongs to. */
private final Class<? extends EncryptionMethod> method;
/** The type of the salt that is used. */
private SaltType saltType;
/** The length of the salt for SaltType.TEXT salts. */
private Integer saltLength;
/** The usage recommendation. */
private Usage usage;
/** Whether or not the encryption method is restricted to ASCII characters for proper functioning. */
private boolean asciiRestricted;
/** Whether or not the encryption method requires its salt stored separately. */
private boolean hasSeparateSalt;
/** The length of the hash output, based on a test hash (i.e. assumes same length for all hashes.) */
private int hashLength;
public MethodDescription(Class<? extends EncryptionMethod> method) {
this.method = method;
}
// Trivial getters and setters
public Class<? extends EncryptionMethod> getMethod() {
return method;
}
public SaltType getSaltType() {
return saltType;
}
public void setSaltType(SaltType saltType) {
this.saltType = saltType;
}
public Integer getSaltLength() {
return saltLength;
}
public void setSaltLength(int saltLength) {
this.saltLength = saltLength;
}
public Usage getUsage() {
return usage;
}
public void setUsage(Usage usage) {
this.usage = usage;
}
public boolean isAsciiRestricted() {
return asciiRestricted;
}
public void setAsciiRestricted(boolean asciiRestricted) {
this.asciiRestricted = asciiRestricted;
}
public boolean hasSeparateSalt() {
return hasSeparateSalt;
}
public void setHasSeparateSalt(boolean hasSeparateSalt) {
this.hasSeparateSalt = hasSeparateSalt;
}
public int getHashLength() {
return hashLength;
}
public void setHashLength(int hashLength) {
this.hashLength = hashLength;
}
}
@@ -0,0 +1,58 @@
<!-- {gen_warning} -->
<!-- File auto-generated on {gen_date}. See hashmethods/hash_algorithms.tpl.md -->
## Hash Algorithms
AuthMe supports the following hash algorithms for storing your passwords safely.
Algorithm | Recommendation | Hash length | ASCII | | Salt type | Length | Separate?
--------- | -------------- | ----------- | ----- | --- | --------- | ------ | ---------
[#algorithms]
{name} | {recommendation} | {hash_length} | {ascii_restricted} | | {salt_type} | {salt_length} | {separate_salt}
[/#algorithms]
CUSTOM | | | | | | | |
<!-- {gen_warning} -->
### Columns
#### Algorithm
The algorithm is the hashing algorithm used to store passwords with. Default is SHA256 and is recommended.
You can change the hashing algorithm in the config.yml: under `security`, locate `passwordHash`.
#### Recommendation
The recommendation lists our usage recommendation in terms of how secure it is (not how _well_ the algorithm works!).
- Recommended: The hash algorithm appears to be cryptographically secure and is one we recommend.
- Acceptable: There are safer algorithms that can be chosen but using the algorithm is generally OK.
- Do not use: Hash algorithm isn't sufficiently secure. Use only if required to hook into another system.
- Does not work: The algorithm does not work properly; do not use.
#### Hash Length
The length of the hashes the algorithm produces. Note that the hash length is not (primarily) indicative of
whether an algorithm is secure or not.
#### ASCII
If denoted with a **y**, means that the algorithm is restricted to ASCII characters only, i.e. it will simply ignore
"special characters" such as `ÿ` or `Â`. Note that we do not recommend the use of "special characters" in passwords.
#### Salt Columns
Before hashing, a _salt_ may be appended to the password to make the hash more secure. The following columns describe
the salt the algorithm uses.
<!-- {gen_warning} -->
##### Salt Type
We do not recommend the usage
of any algorithm that doesn't use a randomly generated text as salt. This "salt type" column indicates what type of
salt the algorithm uses:
- Text: randomly generated text (see also the following column, "Length")
- Username: the salt is constructed from the username (bad)
- None: the algorithm uses no salt (bad)
##### Length
If applicable (salt type is "Text"), indicates the length of the generated salt. The longer the better.
If this column is empty when the salt type is "Text", it typically means the salt length can be defined in config.yml.
##### Separate
If denoted with a **y**, it means that the salt is stored in a separate column in the database. This is neither good
or bad.
{gen_footer}
@@ -0,0 +1,194 @@
package tools.messages;
import com.google.common.base.Predicate;
import com.google.common.collect.HashMultimap;
import com.google.common.collect.Iterables;
import com.google.common.collect.Multimap;
import fr.xephi.authme.output.MessageKey;
import fr.xephi.authme.util.StringUtils;
import org.bukkit.configuration.file.FileConfiguration;
import org.bukkit.configuration.file.YamlConfiguration;
import tools.utils.FileUtils;
import java.io.File;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
/**
* Verifies a message file's keys to ensure that it is in sync with {@link MessageKey}, i.e. that the file contains
* all keys and that it doesn't have any unknown ones.
*/
public class MessageFileVerifier {
private final String messagesFile;
private final Set<String> unknownKeys = new HashSet<>();
// Map with the missing key and a boolean indicating whether or not it was added to the file by this object
private final Map<String, Boolean> missingKeys = new HashMap<>();
private final Multimap<String, String> missingTags = HashMultimap.create();
/**
* Create a verifier that verifies the given messages file.
*
* @param messagesFile The messages file to process
*/
public MessageFileVerifier(String messagesFile) {
this.messagesFile = messagesFile;
verifyKeys();
}
/**
* Return the list of unknown keys, i.e. the list of keys present in the file that are not
* part of the {@link MessageKey} enum.
*
* @return List of unknown keys
*/
public Set<String> getUnknownKeys() {
return unknownKeys;
}
/**
* Return the list of missing keys, i.e. all keys that are part of {@link MessageKey} but absent
* in the messages file.
*
* @return The list of missing keys in the file
*/
public Map<String, Boolean> getMissingKeys() {
return missingKeys;
}
/**
* Return the collection of tags the message key defines that aren't present in the read line.
*
* @return Collection of missing tags per message key. Key = message key, value = missing tag.
*/
public Multimap<String, String> getMissingTags() {
return missingTags;
}
private void verifyKeys() {
FileConfiguration configuration = YamlConfiguration.loadConfiguration(new File(messagesFile));
// Check known keys (their existence + presence of all tags)
for (MessageKey messageKey : MessageKey.values()) {
final String key = messageKey.getKey();
if (configuration.isString(key)) {
checkTagsInMessage(messageKey, configuration.getString(key));
} else {
missingKeys.put(key, false);
}
}
// Check FileConfiguration for all of its keys to find unknown keys
for (String key : configuration.getValues(true).keySet()) {
if (!messageKeyExists(key)) {
unknownKeys.add(key);
}
}
}
private void checkTagsInMessage(MessageKey messageKey, String message) {
for (String tag : messageKey.getTags()) {
if (!message.contains(tag)) {
missingTags.put(messageKey.getKey(), tag);
}
}
}
/**
* Add missing keys to the file with the provided default (English) message.
*
* @param defaultMessages The collection of default messages
*/
public void addMissingKeys(FileConfiguration defaultMessages) {
final List<String> fileLines = new ArrayList<>(
Arrays.asList(FileUtils.readFromFile(messagesFile).split("\\n")));
List<String> keysToAdd = new ArrayList<>();
for (Map.Entry<String, Boolean> entry : missingKeys.entrySet()) {
final String key = entry.getKey();
if (Boolean.FALSE.equals(entry.getValue()) && defaultMessages.get(key) != null) {
keysToAdd.add(key);
}
}
// Add missing keys as comments to the bottom of the file
for (String keyToAdd : keysToAdd) {
int indexOfComment = Iterables.indexOf(fileLines, isCommentFor(keyToAdd));
if (indexOfComment != -1) {
// Comment for keyToAdd already exists, so remove it since we're going to add it
fileLines.remove(indexOfComment);
}
String comment = commentForKey(keyToAdd) + "'" +
defaultMessages.getString(keyToAdd).replace("'", "''") + "'";
fileLines.add(comment);
missingKeys.put(keyToAdd, Boolean.TRUE);
}
// Add a comment above messages missing a tag
for (Map.Entry<String, Collection<String>> entry : missingTags.asMap().entrySet()) {
final String key = entry.getKey();
addCommentForMissingTags(fileLines, key, entry.getValue());
}
FileUtils.writeToFile(messagesFile, StringUtils.join("\n", fileLines));
}
/**
* Add a comment above a message to note the tags the message is missing. Removes
* any similar comment that may already be above the message.
*
* @param fileLines The lines of the file (to modify)
* @param key The key of the message
* @param tags The missing tags
*/
private void addCommentForMissingTags(List<String> fileLines, final String key, Collection<String> tags) {
int indexForComment = Iterables.indexOf(fileLines, isCommentFor(key));
if (indexForComment == -1) {
indexForComment = Iterables.indexOf(fileLines, new Predicate<String>() {
@Override
public boolean apply(String input) {
return input.startsWith(key + ": ");
}
});
if (indexForComment == -1) {
System.err.println("Error adding comment for key '" + key + "': couldn't find entry in file lines");
return;
}
} else {
fileLines.remove(indexForComment);
}
String tagWord = tags.size() > 1 ? "tags" : "tag";
fileLines.add(indexForComment, commentForKey(key)
+ String.format("Missing %s %s", tagWord, StringUtils.join(", ", tags)));
}
private static String commentForKey(String key) {
return String.format("# TODO %s: ", key);
}
private static Predicate<String> isCommentFor(final String key) {
return new Predicate<String>() {
@Override
public boolean apply(String input) {
return input.startsWith(commentForKey(key));
}
};
}
private static boolean messageKeyExists(String key) {
for (MessageKey messageKey : MessageKey.values()) {
if (messageKey.getKey().equals(key)) {
return true;
}
}
return false;
}
}
+2
View File
@@ -0,0 +1,2 @@
## Messages
Verifies the messages files and adds any missing indices with the English content as default.
@@ -0,0 +1,150 @@
package tools.messages;
import com.google.common.collect.Multimap;
import fr.xephi.authme.util.StringUtils;
import org.bukkit.configuration.file.FileConfiguration;
import org.bukkit.configuration.file.YamlConfiguration;
import tools.utils.ToolTask;
import tools.utils.ToolsConstants;
import java.io.File;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.Scanner;
import java.util.Set;
import java.util.regex.Pattern;
import static java.lang.String.format;
/**
* Task to verify the keys in the messages files.
*/
public final class VerifyMessagesTask implements ToolTask {
/** The folder containing the message files. */
private static final String MESSAGES_FOLDER = ToolsConstants.MAIN_RESOURCES_ROOT + "messages/";
/** Pattern of the message file names. */
private static final Pattern MESSAGE_FILE_PATTERN = Pattern.compile("messages_[a-z]{2,7}\\.yml");
/** Tag that is replaced to the messages folder in user input. */
private static final String SOURCES_TAG = "{msgdir}";
/** File to get default messages from (assumes that it is complete). */
private static final String DEFAULT_MESSAGES_FILE = MESSAGES_FOLDER + "messages_en.yml";
@Override
public String getTaskName() {
return "verifyMessages";
}
@Override
public void execute(Scanner scanner) {
System.out.println("Check a specific file only?");
System.out.println("- Empty line will check all files in the resources messages folder (default)");
System.out.println(format("- %s will be replaced to the messages folder %s", SOURCES_TAG, MESSAGES_FOLDER));
String inputFile = scanner.nextLine();
System.out.println("Add any missing keys to files? ['y' = yes]");
boolean addMissingKeys = "y".equalsIgnoreCase(scanner.nextLine());
List<File> messageFiles;
if (StringUtils.isEmpty(inputFile)) {
messageFiles = getMessagesFiles();
} else {
File customFile = new File(inputFile.replace(SOURCES_TAG, MESSAGES_FOLDER));
messageFiles = Collections.singletonList(customFile);
}
FileConfiguration defaultMessages = null;
if (addMissingKeys) {
defaultMessages = YamlConfiguration.loadConfiguration(new File(DEFAULT_MESSAGES_FILE));
}
// Verify the given files
for (File file : messageFiles) {
System.out.println("Verifying '" + file.getName() + "'");
MessageFileVerifier verifier = new MessageFileVerifier(file.getAbsolutePath());
if (addMissingKeys) {
verifyFileAndAddKeys(verifier, defaultMessages);
} else {
verifyFile(verifier);
}
}
if (messageFiles.size() > 1) {
System.out.println("Checked " + messageFiles.size() + " files");
}
}
private static void verifyFile(MessageFileVerifier verifier) {
Map<String, Boolean> missingKeys = verifier.getMissingKeys();
if (!missingKeys.isEmpty()) {
System.out.println(" Missing keys: " + missingKeys.keySet());
}
Set<String> unknownKeys = verifier.getUnknownKeys();
if (!unknownKeys.isEmpty()) {
System.out.println(" Unknown keys: " + unknownKeys);
}
Multimap<String, String> missingTags = verifier.getMissingTags();
for (Map.Entry<String, String> entry : missingTags.entries()) {
System.out.println(" Missing tag '" + entry.getValue() + "' in entry with key '" + entry.getKey() + "'");
}
}
public static void verifyFileAndAddKeys(MessageFileVerifier verifier, FileConfiguration defaultMessages) {
Map<String, Boolean> missingKeys = verifier.getMissingKeys();
if (!missingKeys.isEmpty() || !verifier.getMissingTags().isEmpty()) {
verifier.addMissingKeys(defaultMessages);
List<String> addedKeys = getKeysWithValue(Boolean.TRUE, missingKeys);
System.out.println(" Added missing keys " + addedKeys);
List<String> unsuccessfulKeys = getKeysWithValue(Boolean.FALSE, missingKeys);
if (!unsuccessfulKeys.isEmpty()) {
System.err.println(" Warning! Could not add all missing keys (problem with loading " +
"default messages?)");
System.err.println(" Could not add keys " + unsuccessfulKeys);
}
}
Set<String> unknownKeys = verifier.getUnknownKeys();
if (!unknownKeys.isEmpty()) {
System.out.println(" Unknown keys: " + unknownKeys);
}
Multimap<String, String> missingTags = verifier.getMissingTags();
for (Map.Entry<String, String> entry : missingTags.entries()) {
System.out.println(" Missing tag '" + entry.getValue() + "' in entry with key '" + entry.getKey() + "'");
}
}
private static <K, V> List<K> getKeysWithValue(V value, Map<K, V> map) {
List<K> result = new ArrayList<>();
for (Map.Entry<K, V> entry : map.entrySet()) {
if (value.equals(entry.getValue())) {
result.add(entry.getKey());
}
}
return result;
}
private static List<File> getMessagesFiles() {
File folder = new File(MESSAGES_FOLDER);
File[] files = folder.listFiles();
if (files == null) {
throw new RuntimeException("Could not read files from folder '" + folder.getName() + "'");
}
List<File> messageFiles = new ArrayList<>();
for (File file : files) {
if (MESSAGE_FILE_PATTERN.matcher(file.getName()).matches()) {
messageFiles.add(file);
}
}
if (messageFiles.isEmpty()) {
throw new RuntimeException("Error getting message files: list of files is empty");
}
return messageFiles;
}
}
@@ -0,0 +1,58 @@
package tools.messages.translation;
import org.bukkit.configuration.InvalidConfigurationException;
import org.bukkit.configuration.file.YamlConfiguration;
import org.yaml.snakeyaml.DumperOptions;
import org.yaml.snakeyaml.Yaml;
import java.io.File;
import java.io.IOException;
import java.util.regex.Pattern;
/**
* Extension of {@link YamlConfiguration} to customize the writing style.
*/
public class AuthMeYamlConfiguration extends YamlConfiguration {
// Differences to YamlConfiguration: Texts are always in single quotes
// and line breaks are only applied after 200 chars
@Override
public String saveToString() {
DumperOptions options = new DumperOptions();
options.setDefaultFlowStyle(DumperOptions.FlowStyle.BLOCK);
options.setDefaultScalarStyle(DumperOptions.ScalarStyle.SINGLE_QUOTED);
options.setPrettyFlow(true);
options.setWidth(200);
Yaml yaml = new Yaml(options);
String header = buildHeader();
String dump = yaml.dump(getValues(false));
if (dump.equals(BLANK_CONFIG)) {
dump = "";
}
// By setting the scalar style to SINGLE_QUOTED both keys and values will be enclosed in single quotes.
// We want all texts wrapped in single quotes, but not the keys. Seems like this is not possible in SnakeYAML
dump = Pattern.compile("^'([a-zA-Z0-9-_]+)': ", Pattern.MULTILINE)
.matcher(dump).replaceAll("$1: ");
return header + dump;
}
/**
* Behaves similarly to {@link YamlConfiguration#loadConfiguration(File)} but returns an object
* of this class instead.
*
* @param file the file to load
* @return the constructed AuthMeYamlConfiguration instance
*/
public static AuthMeYamlConfiguration loadConfiguration(File file) {
AuthMeYamlConfiguration config = new AuthMeYamlConfiguration();
try {
config.load(file);
} catch (IOException | InvalidConfigurationException ex) {
throw new IllegalStateException(ex);
}
return config;
}
}
@@ -0,0 +1,106 @@
package tools.messages.translation;
import com.google.common.io.CharStreams;
import com.google.gson.Gson;
import fr.xephi.authme.output.MessageKey;
import fr.xephi.authme.util.StringUtils;
import org.bukkit.configuration.file.FileConfiguration;
import org.bukkit.configuration.file.YamlConfiguration;
import tools.utils.ToolTask;
import tools.utils.ToolsConstants;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.net.HttpURLConnection;
import java.net.URL;
import java.net.URLEncoder;
import java.util.ArrayList;
import java.util.List;
import java.util.Scanner;
/**
* Task to export a language's messages to the remote translation service.
*/
public class ExportMessagesTask implements ToolTask {
/** The folder containing the messages files. */
protected static final String MESSAGES_FOLDER = ToolsConstants.MAIN_RESOURCES_ROOT + "messages/";
/** The remote URL to send an updated file to. */
private static final String UPDATE_URL = "http://jalu.ch/ext/authme/update.php";
private final Gson gson = new Gson();
@Override
public String getTaskName() {
return "exportMessages";
}
@Override
public void execute(Scanner scanner) {
System.out.println("Enter language code of messages to export:");
String languageCode = scanner.nextLine().trim();
File file = new File(MESSAGES_FOLDER + "messages_" + languageCode + ".yml");
if (!file.exists()) {
throw new IllegalStateException("File '" + file.getAbsolutePath() + "' does not exist");
}
FileConfiguration configuration = YamlConfiguration.loadConfiguration(file);
String json = convertToJson(languageCode, loadDefaultMessages(), configuration);
String result = sendJsonToRemote(languageCode, json);
System.out.println("Answer: " + result);
}
protected String convertToJson(String code, FileConfiguration defaultMessages, FileConfiguration messageFile) {
List<MessageExport> list = new ArrayList<>();
for (MessageKey key : MessageKey.values()) {
list.add(new MessageExport(key.getKey(), key.getTags(), getString(key, defaultMessages),
getString(key, messageFile)));
}
return gson.toJson(new LanguageExport(code, list));
}
protected FileConfiguration loadDefaultMessages() {
return YamlConfiguration.loadConfiguration(new File(MESSAGES_FOLDER + "messages_en.yml"));
}
private static String getString(MessageKey key, FileConfiguration configuration) {
return configuration.getString(key.getKey(), "");
}
private static String sendJsonToRemote(String language, String json) {
try {
String encodedData = "file=" + URLEncoder.encode(json, "UTF-8")
+ "&language=" + URLEncoder.encode(language, "UTF-8");
URL url = new URL(UPDATE_URL);
HttpURLConnection conn = (HttpURLConnection) url.openConnection();
conn.setDoOutput(true);
conn.setRequestMethod("POST");
conn.setRequestProperty("Content-Type", "application/x-www-form-urlencoded");
conn.setRequestProperty("Content-Length", String.valueOf(encodedData.length()));
OutputStream os = conn.getOutputStream();
os.write(encodedData.getBytes());
os.flush();
os.close();
return "Response code: " + conn.getResponseCode()
+ "\n" + inputStreamToString(conn.getInputStream());
} catch (IOException e) {
throw new IllegalStateException(e);
}
}
private static String inputStreamToString(InputStream is) {
try (InputStreamReader isr = new InputStreamReader(is)) {
return CharStreams.toString(isr);
} catch (IOException e) {
return "Failed to read output - " + StringUtils.formatException(e);
}
}
}
@@ -0,0 +1,116 @@
package tools.messages.translation;
import com.google.common.io.Resources;
import com.google.gson.Gson;
import fr.xephi.authme.output.MessageKey;
import org.bukkit.configuration.file.FileConfiguration;
import org.bukkit.configuration.file.YamlConfiguration;
import tools.messages.MessageFileVerifier;
import tools.messages.VerifyMessagesTask;
import tools.utils.FileUtils;
import tools.utils.ToolTask;
import tools.utils.ToolsConstants;
import java.io.File;
import java.io.IOException;
import java.net.URL;
import java.nio.charset.Charset;
import java.util.HashSet;
import java.util.Scanner;
import java.util.Set;
import java.util.regex.Pattern;
/**
* Imports a message file from a remote JSON export and validates the resulting file.
* <p>
* Comments at the top of an existing file should remain after the import, but it is important
* to verify that no unwanted changes have been applied to the file. Note that YAML comments
* tend to disappear if there is no space between the <code>#</code> and the first character.
*/
public class ImportMessagesTask implements ToolTask {
private static final String MESSAGES_FOLDER = ToolsConstants.MAIN_RESOURCES_ROOT + "messages/";
private Gson gson = new Gson();
private Set<String> messageCodes;
@Override
public String getTaskName() {
return "importMessages";
}
@Override
public void execute(Scanner scanner) {
System.out.println("Enter URL to import from");
// Dirty trick: replace https:// with http:// so we don't have to worry about installing certificates...
String url = scanner.nextLine().replace("https://", "http://");
LanguageExport languageExport = getLanguageExportFromUrl(url);
if (languageExport == null) {
throw new IllegalStateException("An error occurred: constructed language export is null");
}
mergeExportIntoFile(languageExport);
System.out.println("Saved to messages file for code '" + languageExport.code + "'");
}
private LanguageExport getLanguageExportFromUrl(String location) {
try {
URL url = new URL(location);
String json = Resources.toString(url, Charset.forName("UTF-8"));
return gson.fromJson(json, LanguageExport.class);
} catch (IOException e) {
throw new IllegalStateException(e);
}
}
private void mergeExportIntoFile(LanguageExport export) {
String languageCode = export.code;
String fileName = MESSAGES_FOLDER + "messages_" + languageCode + ".yml";
File file = new File(fileName);
FileConfiguration fileConfiguration;
if (file.exists()) {
removeAllTodoComments(fileName);
fileConfiguration = AuthMeYamlConfiguration.loadConfiguration(file);
} else {
fileConfiguration = new AuthMeYamlConfiguration();
}
buildMessageCodeList();
for (MessageExport messageExport : export.messages) {
if (!messageCodes.contains(messageExport.key)) {
throw new IllegalStateException("Message key '" + messageExport.key + "' does not exist");
} else if (!messageExport.translatedMessage.isEmpty()) {
fileConfiguration.set(messageExport.key, messageExport.translatedMessage);
}
}
try {
fileConfiguration.save(file);
} catch (IOException e) {
throw new IllegalStateException(e);
}
MessageFileVerifier verifier = new MessageFileVerifier(fileName);
VerifyMessagesTask.verifyFileAndAddKeys(verifier, YamlConfiguration.loadConfiguration(
new File(MESSAGES_FOLDER + "messages_en.yml")));
}
private void buildMessageCodeList() {
messageCodes = new HashSet<>(MessageKey.values().length);
for (MessageKey messageKey : MessageKey.values()) {
messageCodes.add(messageKey.getKey());
}
}
/**
* Removes all to-do comments written by {@link VerifyMessagesTask}. This is helpful as the YamlConfiguration
* moves those comments otherwise upon saving.
*
* @param file The file whose to-do comments should be removed
*/
private static void removeAllTodoComments(String file) {
String contents = FileUtils.readFromFile(file);
String regex = "^# TODO .*$";
contents = Pattern.compile(regex, Pattern.MULTILINE).matcher(contents).replaceAll("");
FileUtils.writeToFile(file, contents);
}
}
@@ -0,0 +1,19 @@
package tools.messages.translation;
import java.util.Collections;
import java.util.List;
/**
* Export of a language's messages.
*/
public class LanguageExport {
public final String code;
public final List<MessageExport> messages;
public LanguageExport(String code, List<MessageExport> messages) {
this.code = code;
this.messages = Collections.unmodifiableList(messages);
}
}
@@ -0,0 +1,22 @@
package tools.messages.translation;
import fr.xephi.authme.util.StringUtils;
/**
* Container class for one translatable message.
*/
public class MessageExport {
public final String key;
public final String tags;
public final String defaultMessage;
public final String translatedMessage;
public MessageExport(String key, String[] tags, String defaultMessage, String translatedMessage) {
this.key = key;
this.tags = StringUtils.join(",", tags);
this.defaultMessage = defaultMessage;
this.translatedMessage = translatedMessage;
}
}
@@ -0,0 +1,37 @@
package tools.messages.translation;
import org.bukkit.configuration.file.FileConfiguration;
import org.bukkit.configuration.file.YamlConfiguration;
import tools.utils.FileUtils;
import tools.utils.ToolsConstants;
import java.io.File;
import java.util.Scanner;
/**
* Task which exports all messages to a local folder.
*/
public class WriteAllExportsTask extends ExportMessagesTask {
private static final String OUTPUT_FOLDER = ToolsConstants.TOOLS_SOURCE_ROOT + "messages/translation/export/";
@Override
public String getTaskName() {
return "writeAllExports";
}
@Override
public void execute(Scanner scanner) {
File[] messageFiles = new File(MESSAGES_FOLDER).listFiles();
if (messageFiles == null || messageFiles.length == 0) {
throw new IllegalStateException("Could not read messages folder");
}
final FileConfiguration defaultMessages = loadDefaultMessages();
for (File file : messageFiles) {
String code = file.getName().substring("messages_".length(), file.getName().length() - ".yml".length());
String json = convertToJson(code, defaultMessages, YamlConfiguration.loadConfiguration(file));
FileUtils.writeToFile(OUTPUT_FOLDER + "messages_" + code + ".json", json);
}
}
}
@@ -0,0 +1,106 @@
package tools.permissions;
import fr.xephi.authme.permission.AdminPermission;
import fr.xephi.authme.permission.PermissionNode;
import fr.xephi.authme.permission.PlayerPermission;
import fr.xephi.authme.permission.PlayerStatePermission;
import tools.utils.FileUtils;
import tools.utils.ToolsConstants;
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 {
/**
* Regular expression that should match the JavaDoc comment above an enum, <i>including</i>
* 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<String> gatherNodes() {
Set<String> 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<String, String> gatherNodesWithJavaDoc() {
Map<String, String> result = new TreeMap<>();
result.put("authme.admin.*", "Give access to all admin commands.");
result.put("authme.player.*", "Permission to use all player (non-admin) commands.");
// TODO ljacqu 20160109: Add authme.player.email manual description?
addDescriptionsForClass(PlayerPermission.class, result);
addDescriptionsForClass(AdminPermission.class, result);
addDescriptionsForClass(PlayerStatePermission.class, result);
return result;
}
private <T extends Enum<T> & PermissionNode> void addDescriptionsForClass(Class<T> clazz,
Map<String, String> descriptions) {
String classSource = getSourceForClass(clazz);
Map<String, String> 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<String, String> extractJavaDocFromSource(String source) {
Map<String, String> 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 <T> The concrete type
* @return Source code of the file
*/
private static <T extends Enum<T> & PermissionNode> String getSourceForClass(Class<T> clazz) {
String classFile = ToolsConstants.MAIN_SOURCE_ROOT + clazz.getName().replace(".", "/") + ".java";
return FileUtils.readFromFile(classFile);
}
}
@@ -0,0 +1,84 @@
package tools.permissions;
import tools.utils.FileUtils;
import tools.utils.TagValue.NestedTagValue;
import tools.utils.TagValueHolder;
import tools.utils.ToolTask;
import tools.utils.ToolsConstants;
import java.util.Map;
import java.util.Scanner;
import java.util.Set;
/**
* Task responsible for formatting a permissions node list and
* for writing it to a file if desired.
*/
public class PermissionsListWriter implements ToolTask {
private static final String PERMISSIONS_OUTPUT_FILE = ToolsConstants.DOCS_FOLDER + "permission_nodes.md";
@Override
public String getTaskName() {
return "writePermissionsList";
}
@Override
public void execute(Scanner scanner) {
// Ask if result should be written to file
System.out.println("Include description? [Enter 'n' for no]");
boolean includeDescription = !matches("n", scanner);
boolean writeToFile = false;
if (includeDescription) {
System.out.println("Write to file? [Enter 'n' for no]");
writeToFile = !matches("n", scanner);
}
if (!includeDescription) {
outputSimpleList();
} else if (writeToFile) {
generateAndWriteFile();
} else {
System.out.println(generatePermissionsList());
}
}
private static void generateAndWriteFile() {
final NestedTagValue permissionsTagValue = generatePermissionsList();
TagValueHolder tags = TagValueHolder.create().put("nodes", permissionsTagValue);
FileUtils.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 NestedTagValue generatePermissionsList() {
PermissionNodesGatherer gatherer = new PermissionNodesGatherer();
Map<String, String> permissions = gatherer.gatherNodesWithJavaDoc();
NestedTagValue permissionTags = new NestedTagValue();
for (Map.Entry<String, String> entry : permissions.entrySet()) {
permissionTags.add(TagValueHolder.create()
.put("node", entry.getKey())
.put("description", entry.getValue()));
}
return permissionTags;
}
private static void outputSimpleList() {
PermissionNodesGatherer gatherer = new PermissionNodesGatherer();
Set<String> 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);
}
}
@@ -0,0 +1,2 @@
# About
Helper script to generate a page with an up-to-date list of permission nodes.
@@ -0,0 +1,11 @@
<!-- {gen_warning} -->
<!-- File auto-generated on {gen_date}. See permissions/permission_nodes.tpl.md -->
## AuthMe Permission Nodes
The following are the permission nodes that are currently supported by the latest dev builds.
[#nodes]
- **{node}** {description}
[/#nodes]
{gen_footer}
@@ -0,0 +1,11 @@
#!/bin/sh
#
# Usage: ./analyze_project.sh
#
if [ -z $jarfile ];
then
./setvars.sh
fi
mvn clean verify sonar:sonar -f $pomfile
@@ -0,0 +1,11 @@
#!/bin/sh
#
# Usage: ./build_project.sh
#
if [ -z $jarfile ];
then
./setvars.sh
fi
mvn clean install -f $pomfile -B
@@ -0,0 +1,2 @@
#!/bin/sh
ls -aB *.sh
@@ -0,0 +1,16 @@
#!/bin/sh
#
# Usage: ./move_plugin.sh
#
if [ -z $jarfile ];
then
./setvars.sh
fi
if [ -f $jarfile ]
then
cp $jarfile $plugins
else
echo "Target file not found: $jarfile"
fi
@@ -0,0 +1,11 @@
#!/bin/sh
#
# Usage: ./analyze_project.sh
#
if [ -z $jarfile ];
then
./setvars.sh
fi
mvn install -f $pomfile -Dmaven.test.skip
@@ -0,0 +1,11 @@
#!/bin/sh
if [ -z $jarfile ]
then
./setvars.sh
fi
cd $server
java -Xmx1024M -Xms1024M -jar spigot_server.jar
cd $batdir
./list_files.sh
+16
View File
@@ -0,0 +1,16 @@
#!/bin/sh
#The folder in which these .sh files are located
EXPORT batdir=/home/yourhome/IdeaProjects/AuthMeReloaded/src/tools/shhelpers/
#The location of the generated JAR file
EXPORT jarfile=/home/yourhome/IdeaProjects/AuthMeReloaded/target/AuthMe-5.2-SNAPSHOT.jar
#The location of the pom.xml file of the project
EXPORT pomfile=/home/yourhome/IdeaProjects/AuthMeReloaded/pom.xml
#The folder in which the server is located
EXPORT server=/home/yourhome/AUTHME_DEV/spigot-server/
#The Location of the plugins folder of the Minecraft server
EXPORT plugins=$server/plugins/
@@ -0,0 +1,6 @@
#!/bin/sh
#
# That script sort the content of all files in the current directory
#
ls | grep -v .sort | while read file; do sort "$file" > "$file".sort; done
for file in *.sort; do mv "$file" "${file%%.sort}"; done
+58
View File
@@ -0,0 +1,58 @@
package tools.utils;
import java.io.IOException;
import java.nio.charset.Charset;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.nio.file.StandardOpenOption;
import java.util.List;
/**
* Utility class for reading from and writing to files.
*/
public final class FileUtils {
private final static Charset CHARSET = Charset.forName("utf-8");
private FileUtils() {
}
public static void generateFileFromTemplate(String templateFile, String destinationFile, TagValueHolder tags) {
String template = readFromFile(templateFile);
String result = TagReplacer.applyReplacements(template, tags);
writeToFile(destinationFile, result);
}
public 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 void appendToFile(String outputFile, String contents) {
try {
Files.write(Paths.get(outputFile), contents.getBytes(), StandardOpenOption.APPEND);
} catch (IOException e) {
throw new RuntimeException("Failed to append 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 List<String> readLinesFromFile(String file) {
try {
return Files.readAllLines(Paths.get(file), CHARSET);
} catch (IOException e) {
throw new RuntimeException("Could not read from file '" + file + "'", e);
}
}
}
+110
View File
@@ -0,0 +1,110 @@
package tools.utils;
import tools.utils.TagValue.NestedTagValue;
import tools.utils.TagValue.TextTagValue;
import java.util.Date;
import java.util.Map;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
/**
* Class responsible for replacing template tags to actual content.
* For all files, the following tags are defined:
* <ul>
* <li>{gen_date} the generation date</li>
* <li>{gen_warning} - warning not to edit the generated file directly</li>
* <li>{gen_footer} - info footer with a link to the dev repo so users can find the most up-to-date
* version (in case the page is viewed on a fork)</li>
* </ul>
*/
public class TagReplacer {
private TagReplacer() {
}
/**
* Replace a template with default tags and custom ones supplied by a map.
*
* @param template The template to process
* @param tagValues Container with tags and their associated values
* @return The filled template
*/
public static String applyReplacements(String template, TagValueHolder tagValues) {
String result = template;
for (Map.Entry<String, TagValue<?>> tagRule : tagValues.getValues().entrySet()) {
final String name = tagRule.getKey();
if (tagRule.getValue() instanceof TextTagValue) {
final TextTagValue value = (TextTagValue) tagRule.getValue();
result = replaceOptionalTag(result, name, value)
.replace("{" + name + "}", value.getValue());
} else if (tagRule.getValue() instanceof NestedTagValue) {
final NestedTagValue value = (NestedTagValue) tagRule.getValue();
result = replaceIterateTag(replaceOptionalTag(result, name, value), name, value);
} else {
throw new IllegalStateException("Unknown tag value type");
}
}
return applyReplacements(result);
}
/**
* Apply the default tag replacements.
*
* @param template The template to process
* @return The filled template
*/
public static String applyReplacements(String template) {
String curDate = new Date().toString();
return template
.replace("{gen_date}", curDate)
.replace("{gen_warning}", "AUTO-GENERATED FILE! Do not edit this directly")
.replace("{gen_footer}", "---\n\nThis page was automatically generated on the"
+ " [AuthMe-Team/AuthMeReloaded repository](" + ToolsConstants.DOCS_FOLDER_URL + ")"
+ " on " + curDate);
}
private static String replaceOptionalTag(String text, String tagName, TagValue<?> tagValue) {
Pattern regex = Pattern.compile("\\[" + tagName + "](.*?)\\[/" + tagName + "]", Pattern.DOTALL);
Matcher matcher = regex.matcher(text);
if (!matcher.find()) {
// Couldn't find results, so just return text as it is
return text;
} else if (tagValue.isEmpty()) {
// Tag is empty, replace [tagName]some_text[/tagName] to nothing
return matcher.replaceAll("");
} else {
// Tag is not empty, so replace [tagName]some_text[/tagName] to some_text
return matcher.replaceAll(matcher.group(1));
}
}
/**
* Replace iterating tags with the value. Tags of the type [#tag]...[/#tag] specify to iterate over the
* entries in {@link NestedTagValue} and to apply any replacements in there.
*
* @param text The file text
* @param tagName The tag name to handle
* @param tagValue The associated value
* @return The text with the applied replacement
*/
private static String replaceIterateTag(String text, String tagName, NestedTagValue tagValue) {
Pattern regex = Pattern.compile("\\[#" + tagName + "](.*?)\\[/#" + tagName + "]\\s?", Pattern.DOTALL);
Matcher matcher = regex.matcher(text);
if (!matcher.find()) {
return text;
} else if (tagValue.isEmpty()) {
return matcher.replaceAll("");
}
final String innerTemplate = matcher.group(1).trim() + "\n";
String result = "";
for (TagValueHolder entry : tagValue.getValue()) {
result += applyReplacements(innerTemplate, entry);
}
return matcher.replaceAll(result);
}
}
+46
View File
@@ -0,0 +1,46 @@
package tools.utils;
import java.util.ArrayList;
import java.util.List;
public abstract class TagValue<T> {
private final T value;
public TagValue(T value) {
this.value = value;
}
public T getValue() {
return value;
}
public abstract boolean isEmpty();
public static final class TextTagValue extends TagValue<String> {
public TextTagValue(String value) {
super(value);
}
@Override
public boolean isEmpty() {
return getValue().isEmpty();
}
}
public static final class NestedTagValue extends TagValue<List<TagValueHolder>> {
public NestedTagValue() {
super(new ArrayList<TagValueHolder>());
}
@Override
public boolean isEmpty() {
return getValue().isEmpty();
}
public void add(TagValueHolder entry) {
getValue().add(entry);
}
}
}
@@ -0,0 +1,34 @@
package tools.utils;
import tools.utils.TagValue.TextTagValue;
import java.util.HashMap;
import java.util.Map;
public class TagValueHolder {
private Map<String, TagValue<?>> values;
private TagValueHolder() {
this.values = new HashMap<>();
}
public static TagValueHolder create() {
return new TagValueHolder();
}
public TagValueHolder put(String key, TagValue<?> value) {
values.put(key, value);
return this;
}
public TagValueHolder put(String key, String value) {
values.put(key, new TextTagValue(value));
return this;
}
public Map<String, TagValue<?>> getValues() {
return values;
}
}
+25
View File
@@ -0,0 +1,25 @@
package tools.utils;
import java.util.Scanner;
/**
* Common interface for tool tasks. Note that the implementing tasks are instantiated
* with the default constructor. It is required that it be public.
*/
public interface ToolTask {
/**
* Return the name of the task.
*
* @return Name of the task
*/
String getTaskName();
/**
* Execute the task.
*
* @param scanner Scanner to prompt the user with for options. Do not close it.
*/
void execute(Scanner scanner);
}
@@ -0,0 +1,21 @@
package tools.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 MAIN_RESOURCES_ROOT = "src/main/resources/";
public static final String TOOLS_SOURCE_ROOT = "src/test/java/tools/";
public static final String DOCS_FOLDER = "docs/";
public static final String DOCS_FOLDER_URL = "https://github.com/AuthMe-Team/AuthMeReloaded/tree/master/docs/";
}