package com.github.czyzby.autumn.nongwt.scanner;
import java.io.File;
import java.io.IOException;
import java.lang.annotation.Annotation;
import java.net.URISyntaxException;
import java.net.URL;
import java.util.Enumeration;
import java.util.LinkedList;
import java.util.Queue;
import java.util.jar.JarEntry;
import java.util.jar.JarFile;
import com.badlogic.gdx.utils.Array;
import com.badlogic.gdx.utils.GdxRuntimeException;
import com.badlogic.gdx.utils.reflect.ClassReflection;
import com.badlogic.gdx.utils.reflect.ReflectionException;
import com.github.czyzby.autumn.scanner.ClassScanner;
import com.github.czyzby.kiwi.util.common.Exceptions;
import com.github.czyzby.kiwi.util.common.Strings;
import com.github.czyzby.kiwi.util.gdx.collection.GdxArrays;
import com.github.czyzby.kiwi.util.tuple.immutable.Pair;
/** Tries to scan class path resources if running from binaries (IDE), or .jar files otherwise. Has no external
* dependencies. Will not work on GWT (is not available there) or mobile platforms.
*
* @author MJ */
public class FallbackDesktopClassScanner implements ClassScanner {
private static final char DOT_SEPARATOR = '.';
private static final char FILE_SEPARATOR = '/';
private static final String CLASS_FILE_EXTENSION = ".class";
private static final String JAR_FILE_EXTENSION = ".jar";
@Override
public Array<Class<?>> findClassesAnnotatedWith(final Class<?> root,
final Iterable<Class<? extends Annotation>> annotations) {
final String mainPackageName = root.getPackage().getName();
final String classPathRoot = getClassPathRoot(mainPackageName);
final ClassLoader classLoader = root.getClassLoader() == null ? ClassLoader.getSystemClassLoader()
: root.getClassLoader();
try {
final Enumeration<URL> resources = classLoader.getResources(classPathRoot);
final Queue<Pair<File, Integer>> filesWithDepthsToProcess = new LinkedList<Pair<File, Integer>>();
while (resources.hasMoreElements()) {
try {
filesWithDepthsToProcess.add(Pair.of(toFile(resources.nextElement()), 0));
} catch (final Exception uriSyntaxException) {
Exceptions.ignore(uriSyntaxException); // Will throw an exception for non-hierarchical files.
}
}
if (filesWithDepthsToProcess.isEmpty()) {
return extractFromJar(annotations, classPathRoot, classLoader);
}
return extractFromBinaries(annotations, mainPackageName, filesWithDepthsToProcess);
} catch (final Throwable exception) {
throw new GdxRuntimeException("Unable to scan classpath.", exception);
}
}
private static Array<Class<?>> extractFromBinaries(final Iterable<Class<? extends Annotation>> annotations,
final String mainPackageName, final Queue<Pair<File, Integer>> filesWithDepthsToProcess)
throws ReflectionException {
final Array<Class<?>> result = GdxArrays.newArray();
while (!filesWithDepthsToProcess.isEmpty()) {
final Pair<File, Integer> classPathFileWithDepth = filesWithDepthsToProcess.poll();
final File classPathFile = classPathFileWithDepth.getFirst();
final int depth = classPathFileWithDepth.getSecond();
if (classPathFile.isDirectory()) {
addAllChildren(filesWithDepthsToProcess, classPathFile, depth);
} else {
final String className = getBinaryClassName(mainPackageName, classPathFile, depth);
if (!isFromPackage(mainPackageName, className)) {
continue;
}
final Class<?> classToProcess = ClassReflection.forName(className);
processClass(annotations, result, classToProcess);
}
}
return result;
}
private static boolean isFromPackage(final String mainPackageName, final String className) {
return !Strings.contains(className, '-') && className.startsWith(mainPackageName);
} // True if not package-info.
private static File toFile(final URL url) throws URISyntaxException {
return new File(url.toURI()).getAbsoluteFile();
}
private static void addAllChildren(final Queue<Pair<File, Integer>> rootFiles, final File classPathFile,
int depth) {
depth++;
for (final File file : classPathFile.listFiles()) {
if (file.isDirectory() || file.getName().endsWith(CLASS_FILE_EXTENSION)) {
rootFiles.add(Pair.of(file, depth));
}
}
}
private static String getBinaryClassName(final String mainPackageName, final File classPathFile, final int depth) {
final String[] classFolders = classPathFile.getPath().split(File.separator);
final StringBuilder builder = new StringBuilder(mainPackageName);
for (int folderIndex = classFolders.length - depth; folderIndex < classFolders.length - 1; folderIndex++) {
builder.append(DOT_SEPARATOR).append(classFolders[folderIndex]);
}
final String classFileName = classFolders[classFolders.length - 1];
builder.append(DOT_SEPARATOR)
.append(classFileName.substring(0, classFileName.length() - CLASS_FILE_EXTENSION.length()));
return builder.toString();
}
private static String getClassPathRoot(final String mainPackageName) {
return mainPackageName.replace(DOT_SEPARATOR, FILE_SEPARATOR);
}
private static Array<Class<?>> extractFromJar(final Iterable<Class<? extends Annotation>> annotations,
final String classPathRoot, final ClassLoader classLoader)
throws URISyntaxException, IOException, ReflectionException {
final Array<JarFile> filesToProcess = getJarFilesToProcess();
final Array<Class<?>> result = GdxArrays.newArray();
for (final JarFile jarFile : filesToProcess) {
final Enumeration<JarEntry> entries = jarFile.entries();
while (entries.hasMoreElements()) {
final JarEntry entry = entries.nextElement();
processEntry(annotations, classPathRoot, result, entry);
}
}
return result;
}
private static Array<JarFile> getJarFilesToProcess() throws URISyntaxException, IOException {
final Array<JarFile> filesToProcess = GdxArrays.newArray();
final File jarDirectory = new File(ClassLoader.getSystemClassLoader().getResource(".").toURI());
for (final File file : jarDirectory.listFiles()) {
if (file.getName().endsWith(JAR_FILE_EXTENSION)) {
filesToProcess.add(new JarFile(file));
}
}
return filesToProcess;
}
private static void processEntry(final Iterable<Class<? extends Annotation>> annotations,
final String classPathRoot, final Array<Class<?>> result, final JarEntry entry) throws ReflectionException {
if (!entry.isDirectory()) {
final String entryName = entry.getName();
if (isFromPackage(classPathRoot, entryName) && entryName.endsWith(CLASS_FILE_EXTENSION)) {
final String className = jarEntryToClassName(entryName);
final Class<?> classToProcess = ClassReflection.forName(className);
processClass(annotations, result, classToProcess);
}
}
}
private static void processClass(final Iterable<Class<? extends Annotation>> annotations,
final Array<Class<?>> result, final Class<?> classToProcess) {
for (final Class<? extends Annotation> annotation : annotations) {
if (ClassReflection.isAnnotationPresent(classToProcess, annotation)) {
result.add(classToProcess);
return;
}
}
}
private static String jarEntryToClassName(final String entryName) {
return entryName.substring(0, entryName.length() - CLASS_FILE_EXTENSION.length()).replace(FILE_SEPARATOR,
DOT_SEPARATOR);
}
}