diff --git a/src/test/java/fr/xephi/authme/ClassCollector.java b/src/test/java/fr/xephi/authme/ClassCollector.java new file mode 100644 index 000000000..9cb9cbb16 --- /dev/null +++ b/src/test/java/fr/xephi/authme/ClassCollector.java @@ -0,0 +1,165 @@ +package fr.xephi.authme; + +import java.io.File; +import java.lang.reflect.Modifier; +import java.util.ArrayList; +import java.util.List; +import java.util.function.Function; +import java.util.function.Predicate; +import java.util.stream.Collectors; + +/** + * Collects available classes by walking through a source directory. + *

+ * This is a naive, zero dependency collector that walks through a file directory + * and loads classes from the class loader based on the .java files it encounters. + * This is a very slow approach and should be avoided for production code. + *

+ * For more performant approaches, see e.g. org.reflections. + */ +public class ClassCollector { + + private final String root; + private final String nonCodePath; + + /** + * Constructor. The arguments make up the path from which the collector will start scanning. + * + * @param nonCodePath beginning of the starting path that are not Java packages, e.g. {@code src/main/java/} + * @param packagePath folders following {@code nonCodePath} that are packages, e.g. {@code com/project/app} + */ + public ClassCollector(String nonCodePath, String packagePath) { + if (!nonCodePath.endsWith("/") && !nonCodePath.endsWith("\\")) { + nonCodePath = nonCodePath.concat(File.separator); + } + this.root = nonCodePath + packagePath; + this.nonCodePath = nonCodePath; + } + + /** + * Collects all classes from the parent folder and below. + * + * @return all classes + */ + public List> collectClasses() { + return collectClasses(x -> true); + } + + /** + * Collects all classes from the parent folder and below which are of type {@link T}. + * + * @param parent the parent which classes need to extend (or be equal to) in order to be collected + * @param the parent type + * @return list of matching classes + */ + @SuppressWarnings("unchecked") + public List> collectClasses(Class parent) { + List> classes = collectClasses(parent::isAssignableFrom); + return new ArrayList<>((List) classes); + } + + /** + * Collects all classes from the parent folder and below which match the given predicate. + * + * @param filter the predicate classes need to satisfy in order to be collected + * @return list of matching classes + */ + public List> collectClasses(Predicate> filter) { + File rootFolder = new File(root); + List> collection = new ArrayList<>(); + collectClasses(rootFolder, filter, collection); + return collection; + } + + /** + * Constructs an instance of all classes which are of the provided type {@code clazz}. + * This method assumes that every class has an accessible no-args constructor for creation. + * + * @param parent the parent which classes need to extend (or be equal to) in order to be instantiated + * @param the parent type + * @return collection of created objects + */ + public List getInstancesOfType(Class parent) { + return getInstancesOfType(parent, (clz) -> { + try { + return canInstantiate(clz) ? clz.newInstance() : null; + } catch (InstantiationException | IllegalAccessException e) { + throw new IllegalStateException(e); + } + }); + } + + /** + * Constructs an instance of all classes which are of the provided type {@code clazz} + * with the provided {@code instantiator}. + * + * @param parent the parent which classes need to extend (or be equal to) in order to be instantiated + * @param instantiator function which returns an object of the given class, or null to skip the class + * @param the parent type + * @return collection of created objects + */ + public List getInstancesOfType(Class parent, Function, T> instantiator) { + return collectClasses(parent) + .stream() + .map(instantiator) + .filter(o -> o != null) + .collect(Collectors.toList()); + } + + /** + * Returns whether the given class can be instantiated, i.e. if it is not abstract, an interface, etc. + * + * @param clazz the class to process + * @return true if the class can be instantiated, false otherwise + */ + public static boolean canInstantiate(Class clazz) { + return clazz != null && !clazz.isEnum() && !clazz.isInterface() + && !clazz.isArray() && !Modifier.isAbstract(clazz.getModifiers()); + } + + /** + * Recursively collects the classes based on the files in the directory and in its child directories. + * + * @param folder the folder to scan + * @param filter the class predicate + * @param collection collection to add classes to + */ + private void collectClasses(File folder, Predicate> filter, List> collection) { + File[] files = folder.listFiles(); + if (files == null) { + throw new IllegalStateException("Could not read files from '" + folder + "'"); + } + for (File file : files) { + if (file.isDirectory()) { + collectClasses(file, filter, collection); + } else if (file.isFile()) { + Class clazz = loadTaskClassFromFile(file); + if (clazz != null && filter.test(clazz)) { + collection.add(clazz); + } + } + } + } + + /** + * Loads a class from the class loader based on the given file. + * + * @param file the file whose corresponding Java class should be retrieved + * @return the corresponding class, or null if not applicable + */ + private Class loadTaskClassFromFile(File file) { + if (!file.getName().endsWith(".java")) { + return null; + } + + String filePath = file.getPath(); + String className = filePath + .substring(nonCodePath.length(), filePath.length() - 5) + .replace(File.separator, "."); + try { + return Class.forName(className); + } catch (ClassNotFoundException e) { + throw new IllegalStateException(e); + } + } +} diff --git a/src/test/java/fr/xephi/authme/TestHelper.java b/src/test/java/fr/xephi/authme/TestHelper.java index 40cff7098..e6435b5a9 100644 --- a/src/test/java/fr/xephi/authme/TestHelper.java +++ b/src/test/java/fr/xephi/authme/TestHelper.java @@ -28,6 +28,8 @@ import static org.mockito.Mockito.verify; */ public final class TestHelper { + public static final String SOURCES_FOLDER = "src/main/java/"; + public static final String TEST_SOURCES_FOLDER = "src/test/java/"; public static final String PROJECT_ROOT = "/fr/xephi/authme/"; private TestHelper() { diff --git a/src/test/java/fr/xephi/authme/events/EventsConsistencyTest.java b/src/test/java/fr/xephi/authme/events/EventsConsistencyTest.java index 0f8340d8b..fc96653b8 100644 --- a/src/test/java/fr/xephi/authme/events/EventsConsistencyTest.java +++ b/src/test/java/fr/xephi/authme/events/EventsConsistencyTest.java @@ -1,14 +1,14 @@ package fr.xephi.authme.events; +import fr.xephi.authme.ClassCollector; +import fr.xephi.authme.TestHelper; import org.apache.commons.lang.reflect.MethodUtils; import org.bukkit.event.Event; import org.junit.BeforeClass; import org.junit.Test; -import java.io.File; import java.lang.reflect.Method; import java.lang.reflect.Modifier; -import java.util.ArrayList; import java.util.List; import static org.hamcrest.Matchers.equalTo; @@ -20,25 +20,14 @@ import static org.junit.Assert.assertThat; */ public class EventsConsistencyTest { - private static final String SRC_FOLDER = "src/main/java/"; - private static final String EVENTS_FOLDER = SRC_FOLDER + "/fr/xephi/authme/events/"; + private static final String EVENTS_FOLDER = TestHelper.PROJECT_ROOT + "events/"; private static List> classes; @BeforeClass public static void scanEventClasses() { - File eventsFolder = new File(EVENTS_FOLDER); - File[] filesInFolder = eventsFolder.listFiles(); - if (filesInFolder == null || filesInFolder.length == 0) { - throw new IllegalStateException("Could not read folder '" + EVENTS_FOLDER + "'. Is it correct?"); - } + ClassCollector classCollector = new ClassCollector(TestHelper.SOURCES_FOLDER, EVENTS_FOLDER); + classes = classCollector.collectClasses(Event.class); - classes = new ArrayList<>(); - for (File file : filesInFolder) { - Class clazz = getEventClassFromFile(file); - if (clazz != null) { - classes.add(clazz); - } - } if (classes.isEmpty()) { throw new IllegalStateException("Did not find any AuthMe event classes. Is the folder correct?"); } @@ -74,22 +63,4 @@ public class EventsConsistencyTest { private static boolean canBeInstantiated(Class clazz) { return !clazz.isInterface() && !clazz.isEnum() && !Modifier.isAbstract(clazz.getModifiers()); } - - @SuppressWarnings("unchecked") - private static Class getEventClassFromFile(File file) { - String fileName = file.getPath(); - String className = fileName - .substring(SRC_FOLDER.length(), fileName.length() - ".java".length()) - .replace(File.separator, "."); - try { - Class clazz = EventsConsistencyTest.class.getClassLoader().loadClass(className); - if (Event.class.isAssignableFrom(clazz)) { - return (Class) clazz; - } - return null; - } catch (ClassNotFoundException e) { - throw new IllegalStateException("Could not load class '" + className + "'", e); - } - } - } diff --git a/src/test/java/fr/xephi/authme/settings/properties/SettingsClassConsistencyTest.java b/src/test/java/fr/xephi/authme/settings/properties/SettingsClassConsistencyTest.java index 0b99ac5c9..73412f172 100644 --- a/src/test/java/fr/xephi/authme/settings/properties/SettingsClassConsistencyTest.java +++ b/src/test/java/fr/xephi/authme/settings/properties/SettingsClassConsistencyTest.java @@ -2,15 +2,14 @@ package fr.xephi.authme.settings.properties; import com.github.authme.configme.SettingsHolder; import com.github.authme.configme.properties.Property; +import fr.xephi.authme.ClassCollector; import fr.xephi.authme.ReflectionTestUtils; import fr.xephi.authme.TestHelper; import org.junit.BeforeClass; import org.junit.Test; -import java.io.File; import java.lang.reflect.Field; import java.lang.reflect.Modifier; -import java.util.ArrayList; import java.util.HashSet; import java.util.List; import java.util.Set; @@ -24,25 +23,14 @@ import static org.junit.Assert.fail; */ public class SettingsClassConsistencyTest { - private static final String SETTINGS_FOLDER = "src/main/java/fr/xephi/authme/settings/properties"; + private static final String SETTINGS_FOLDER = TestHelper.PROJECT_ROOT + "settings/properties"; private static List> classes; @BeforeClass public static void scanForSettingsClasses() { - File settingsFolder = new File(SETTINGS_FOLDER); - File[] filesInFolder = settingsFolder.listFiles(); - if (filesInFolder == null || filesInFolder.length == 0) { - throw new IllegalStateException("Could not read folder '" + SETTINGS_FOLDER + "'. Is it correct?"); - } - - classes = new ArrayList<>(); - for (File file : filesInFolder) { - Class clazz = getSettingsClassFromFile(file); - if (clazz != null) { - classes.add(clazz); - } - } - System.out.println("Found " + classes.size() + " SettingsClass implementations"); + ClassCollector collector = new ClassCollector(TestHelper.SOURCES_FOLDER, SETTINGS_FOLDER); + classes = collector.collectClasses(SettingsHolder.class); + System.out.println("Found " + classes.size() + " SettingsHolder implementations"); } /** @@ -93,22 +81,4 @@ public class SettingsClassConsistencyTest { int modifiers = field.getModifiers(); return Modifier.isPublic(modifiers) && Modifier.isStatic(modifiers) && Modifier.isFinal(modifiers); } - - @SuppressWarnings("unchecked") - private static Class getSettingsClassFromFile(File file) { - String fileName = file.getPath(); - String className = fileName - .substring("src/main/java/".length(), fileName.length() - ".java".length()) - .replace(File.separator, "."); - try { - Class clazz = SettingsClassConsistencyTest.class.getClassLoader().loadClass(className); - if (SettingsHolder.class.isAssignableFrom(clazz)) { - return (Class) clazz; - } - return null; - } catch (ClassNotFoundException e) { - throw new IllegalStateException("Could not load class '" + className + "'", e); - } - } - } diff --git a/src/test/java/tools/ToolsRunner.java b/src/test/java/tools/ToolsRunner.java index b387f32fe..240b0681a 100644 --- a/src/test/java/tools/ToolsRunner.java +++ b/src/test/java/tools/ToolsRunner.java @@ -1,23 +1,23 @@ package tools; +import fr.xephi.authme.ClassCollector; +import fr.xephi.authme.TestHelper; import tools.utils.AutoToolTask; 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.HashMap; import java.util.Map; import java.util.Scanner; -import java.util.TreeMap; /** * Runner for executing tool tasks. */ public final class ToolsRunner { - private ToolsRunner() { + private Map tasks; + + private ToolsRunner(Map tasks) { + this.tasks = tasks; } /** @@ -26,19 +26,23 @@ public final class ToolsRunner { * @param args . */ public static void main(String... args) { - // Collect tasks and show them - File toolsFolder = new File(ToolsConstants.TOOLS_SOURCE_ROOT); - Map tasks = new TreeMap<>(String.CASE_INSENSITIVE_ORDER); - collectTasksInDirectory(toolsFolder, tasks); + // Note ljacqu 20151212: If the tools folder becomes a lot bigger, it will make sense to restrict the depth + // of this recursive collector + ClassCollector collector = new ClassCollector(TestHelper.TEST_SOURCES_FOLDER, "tools"); + Map tasks = new HashMap<>(); + for (ToolTask task : collector.getInstancesOfType(ToolTask.class)) { + tasks.put(task.getTaskName(), task); + } + ToolsRunner runner = new ToolsRunner(tasks); if (args == null || args.length == 0) { - promptAndExecuteTask(tasks); + runner.promptAndExecuteTask(); } else { - executeAutomaticTasks(tasks, args); + runner.executeAutomaticTasks(args); } } - private static void promptAndExecuteTask(Map tasks) { + private void promptAndExecuteTask() { System.out.println("The following tasks are available:"); for (String key : tasks.keySet()) { System.out.println("- " + key); @@ -57,7 +61,7 @@ public final class ToolsRunner { scanner.close(); } - private static void executeAutomaticTasks(Map tasks, String... requests) { + private void executeAutomaticTasks(String... requests) { for (String taskName : requests) { ToolTask task = tasks.get(taskName); if (task == null) { @@ -69,80 +73,4 @@ public final class ToolsRunner { } } } - - /** - * 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 taskCollection) { - File[] files = dir.listFiles(); - if (files == null) { - throw new IllegalStateException("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 taskClass = loadTaskClassFromFile(file); - if (taskClass == null) { - return null; - } - - try { - Constructor constructor = taskClass.getConstructor(); - return constructor.newInstance(); - } catch (NoSuchMethodException | InvocationTargetException | - IllegalAccessException | InstantiationException e) { - throw new IllegalStateException("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 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 = ToolsRunner.class.getClassLoader().loadClass(className); - return ToolTask.class.isAssignableFrom(clazz) && isInstantiable(clazz) - ? (Class) clazz - : null; - } catch (ClassNotFoundException e) { - throw new IllegalStateException(e); - } - } - - private static boolean isInstantiable(Class clazz) { - return !clazz.isInterface() && !Modifier.isAbstract(clazz.getModifiers()); - } - } diff --git a/src/test/java/tools/checktestmocks/CheckTestMocks.java b/src/test/java/tools/checktestmocks/CheckTestMocks.java index 8cd0bad81..8e3011349 100644 --- a/src/test/java/tools/checktestmocks/CheckTestMocks.java +++ b/src/test/java/tools/checktestmocks/CheckTestMocks.java @@ -1,15 +1,14 @@ package tools.checktestmocks; -import com.google.common.base.Function; import com.google.common.collect.Collections2; import com.google.common.collect.Sets; +import fr.xephi.authme.ClassCollector; +import fr.xephi.authme.TestHelper; import fr.xephi.authme.util.StringUtils; import org.mockito.Mock; import tools.utils.AutoToolTask; import tools.utils.InjectorUtils; -import tools.utils.ToolsConstants; -import java.io.File; import java.lang.reflect.Field; import java.util.ArrayList; import java.util.Collection; @@ -38,33 +37,13 @@ public class CheckTestMocks implements AutoToolTask { @Override public void executeDefault() { - readAndCheckFiles(new File(ToolsConstants.TEST_SOURCE_ROOT)); + ClassCollector collector = new ClassCollector(TestHelper.SOURCES_FOLDER, TestHelper.PROJECT_ROOT); + for (Class clazz : collector.collectClasses(c -> isTestClassWithMocks(c))) { + checkClass(clazz); + } System.out.println(StringUtils.join("\n", errors)); } - /** - * Recursively reads directories and checks the contained classes. - * - * @param dir the directory to read - */ - private void readAndCheckFiles(File dir) { - File[] files = dir.listFiles(); - if (files == null) { - throw new IllegalStateException("Cannot read folder '" + dir + "'"); - } - for (File file : files) { - if (file.isDirectory()) { - readAndCheckFiles(file); - } else if (file.isFile()) { - Class clazz = loadTestClass(file); - if (clazz != null) { - checkClass(clazz); - } - // else System.out.format("No @Mock fields found in class of file '%s'%n", file.getName()) - } - } - } - /** * Checks the given test class' @Mock fields against the corresponding production class' @Inject fields. * @@ -91,20 +70,6 @@ public class CheckTestMocks implements AutoToolTask { errors.add(clazz.getSimpleName() + ": " + message); } - private static Class loadTestClass(File file) { - String fileName = file.getPath(); - String className = fileName - // Strip source folders and .java ending - .substring("src/test/java/".length(), fileName.length() - 5) - .replace(File.separator, "."); - try { - Class clazz = CheckTestMocks.class.getClassLoader().loadClass(className); - return isTestClassWithMocks(clazz) ? clazz : null; - } catch (ClassNotFoundException e) { - throw new UnsupportedOperationException(e); - } - } - private static Set> getMocks(Class clazz) { Set> result = new HashSet<>(); for (Field field : clazz.getDeclaredFields()) { @@ -147,12 +112,7 @@ public class CheckTestMocks implements AutoToolTask { } private static String formatClassList(Collection> coll) { - Collection classNames = Collections2.transform(coll, new Function, String>() { - @Override - public String apply(Class input) { - return input.getSimpleName(); - } - }); + Collection classNames = Collections2.transform(coll, Class::getSimpleName); return StringUtils.join(", ", classNames); } diff --git a/src/test/java/tools/dependencygraph/DrawDependency.java b/src/test/java/tools/dependencygraph/DrawDependency.java index 96c490317..6515f52d7 100644 --- a/src/test/java/tools/dependencygraph/DrawDependency.java +++ b/src/test/java/tools/dependencygraph/DrawDependency.java @@ -5,6 +5,8 @@ import ch.jalu.injector.handlers.instantiation.Instantiation; import com.google.common.collect.HashMultimap; import com.google.common.collect.ImmutableList; import com.google.common.collect.Multimap; +import fr.xephi.authme.ClassCollector; +import fr.xephi.authme.TestHelper; import fr.xephi.authme.command.ExecutableCommand; import fr.xephi.authme.converter.Converter; import fr.xephi.authme.initialization.DataFolder; @@ -16,7 +18,6 @@ import tools.utils.InjectorUtils; import tools.utils.ToolTask; import tools.utils.ToolsConstants; -import java.io.File; import java.io.IOException; import java.lang.annotation.Annotation; import java.nio.file.Files; @@ -34,15 +35,12 @@ import java.util.Scanner; public class DrawDependency implements ToolTask { private static final String DOT_FILE = ToolsConstants.TOOLS_SOURCE_ROOT + "dependencygraph/graph.dot"; - // Package root - private static final String ROOT_PACKAGE = "fr.xephi.authme"; private static final List> SUPER_TYPES = ImmutableList.of(ExecutableCommand.class, SynchronousProcess.class, AsynchronousProcess.class, EncryptionMethod.class, Converter.class, Listener.class); /** Annotation types by which dependencies are identified. */ - private static final List> ANNOTATION_TYPES = - ImmutableList.>of(DataFolder.class); + private static final List> ANNOTATION_TYPES = ImmutableList.of(DataFolder.class); private boolean mapToSupertype; // Map with the graph's nodes: value is one of the key's dependencies @@ -59,7 +57,10 @@ public class DrawDependency implements ToolTask { mapToSupertype = "y".equalsIgnoreCase(scanner.nextLine()); // Gather all connections - readAndProcessFiles(new File(ToolsConstants.MAIN_SOURCE_ROOT)); + ClassCollector collector = new ClassCollector(TestHelper.SOURCES_FOLDER, TestHelper.PROJECT_ROOT); + for (Class clazz : collector.collectClasses()) { + processClass(clazz); + } // Prompt user for simplification of graph System.out.println("Do you want to remove classes that are not used as dependency elsewhere?"); @@ -93,28 +94,6 @@ public class DrawDependency implements ToolTask { System.out.format("Run 'dot -Tpng %s -o graph.png' to generate image (requires GraphViz)%n", DOT_FILE); } - /** - * Recursively reads the given directory and processes the files. - * - * @param dir the directory to read - */ - private void readAndProcessFiles(File dir) { - File[] files = dir.listFiles(); - if (files == null) { - throw new IllegalStateException("Cannot read folder '" + dir + "'"); - } - for (File file : files) { - if (file.isDirectory()) { - readAndProcessFiles(file); - } else if (file.isFile()) { - Class clazz = loadClass(file); - if (clazz != null) { - processClass(clazz); - } - } - } - } - private void processClass(Class clazz) { List dependencies = getDependencies(clazz); if (dependencies != null) { @@ -134,21 +113,6 @@ public class DrawDependency implements ToolTask { return clazz; } - // Load Class object for the class in the given file - private static Class loadClass(File file) { - final String fileName = file.getPath().replace(File.separator, "."); - if (!fileName.endsWith(".java")) { - return null; - } - final String className = fileName - .substring(fileName.indexOf(ROOT_PACKAGE), fileName.length() - ".java".length()); - try { - return DrawDependency.class.getClassLoader().loadClass(className); - } catch (ClassNotFoundException e) { - throw new IllegalStateException(e); - } - } - private List getDependencies(Class clazz) { Instantiation instantiation = InjectorUtils.getInstantiationMethod(clazz); return instantiation == null ? null : formatInjectionDependencies(instantiation);