#932 Create class collector and use it where applicable
- Extract logic for walking through a directory and loading its classes into a separate class - Replace all implementations with the new ClassCollector
This commit is contained in:
@@ -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.
|
||||
* <p>
|
||||
* 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.
|
||||
* <p>
|
||||
* For more performant approaches, see e.g. <a href="https://github.com/ronmamo/reflections">org.reflections</a>.
|
||||
*/
|
||||
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<Class<?>> 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 <T> the parent type
|
||||
* @return list of matching classes
|
||||
*/
|
||||
@SuppressWarnings("unchecked")
|
||||
public <T> List<Class<? extends T>> collectClasses(Class<T> parent) {
|
||||
List<Class<?>> 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<Class<?>> collectClasses(Predicate<Class<?>> filter) {
|
||||
File rootFolder = new File(root);
|
||||
List<Class<?>> 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 <T> the parent type
|
||||
* @return collection of created objects
|
||||
*/
|
||||
public <T> List<T> getInstancesOfType(Class<T> 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 <T> the parent type
|
||||
* @return collection of created objects
|
||||
*/
|
||||
public <T> List<T> getInstancesOfType(Class<T> parent, Function<Class<? extends T>, 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<Class<?>> filter, List<Class<?>> 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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() {
|
||||
|
||||
@@ -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<Class<? extends Event>> 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<? extends Event> 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<? extends Event> 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<? extends Event>) clazz;
|
||||
}
|
||||
return null;
|
||||
} catch (ClassNotFoundException e) {
|
||||
throw new IllegalStateException("Could not load class '" + className + "'", e);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
+5
-35
@@ -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<Class<? extends SettingsHolder>> 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<? extends SettingsHolder> 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<? extends SettingsHolder> 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<? extends SettingsHolder>) clazz;
|
||||
}
|
||||
return null;
|
||||
} catch (ClassNotFoundException e) {
|
||||
throw new IllegalStateException("Could not load class '" + className + "'", e);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user