package jaci.openrio.toast.core.loader;
import edu.wpi.first.wpilibj.RobotBase;
import jaci.openrio.toast.core.Toast;
import jaci.openrio.toast.core.ToastBootstrap;
import jaci.openrio.toast.core.io.Storage;
import jaci.openrio.toast.core.loader.annotation.NoLoad;
import jaci.openrio.toast.core.loader.module.ModuleCandidate;
import jaci.openrio.toast.core.loader.module.ModuleContainer;
import jaci.openrio.toast.lib.log.Logger;
import jaci.openrio.toast.lib.module.ModuleWrapper;
import jaci.openrio.toast.lib.module.ToastModule;
import jaci.openrio.toast.lib.profiler.ProfilerEntity;
import jaci.openrio.toast.lib.profiler.ProfilerSection;
import java.io.File;
import java.io.FilenameFilter;
import java.io.IOException;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.net.URL;
import java.net.URLClassLoader;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.ArrayList;
import java.util.Collections;
import java.util.LinkedList;
import java.util.jar.Attributes;
import java.util.jar.JarFile;
import java.util.jar.Manifest;
import java.util.regex.Pattern;
import java.util.zip.ZipEntry;
import static jaci.openrio.toast.core.loader.module.ModuleManager.getCandidates;
import static jaci.openrio.toast.core.loader.module.ModuleManager.getContainers;
/**
* The class responsible for loading modules into Toast. This crawls the Discovery Directories and will also search Manually
* Loaded Classes, of which are added by Launch Arguments.
*
* @author Jaci
*/
public class RobotLoader {
/* EXTERNALS */
public static boolean search = false;
public static ArrayList<String> manualLoadedClasses = new ArrayList<>();
public static ArrayList<String> coreClasses = new ArrayList<>();
public static ArrayList<Object> coreObjects = new ArrayList<>();
public static LinkedList<Method> queuedPrestart = new LinkedList<>();
/* INTERNALS */
private static Logger log;
private static URLClassLoader sysLoader = (URLClassLoader) ClassLoader.getSystemClassLoader();
private static boolean isCorePhase = false;
private static MethodExecutor exec;
public static Pattern classFile = Pattern.compile("([^\\s$]+).class$");
/**
* Also known as the Core pre-init. All initialization is done here.
*/
public static void preinit(ProfilerSection profiler) {
log = new Logger("Toast|Loader", Logger.ATTR_DEFAULT);
if (search)
loadDevEnvironment(profiler);
isCorePhase = true;
loadCandidates(profiler.section("CoreJava"));
parseCoreEntries(profiler.section("CoreJava"));
}
/**
* Sets up non-core modules for loading
*/
public static void init(ProfilerSection section) {
isCorePhase = false;
ProfilerSection section1 = section.section("Java");
loadCandidates(section1);
parseEntries(section1);
constructModules(section1);
resolveBranches(section1);
}
/* Loaders */
/**
* Load a module candidate from a directory. This is usually used in -sim --search.
*/
static void loadDirectory(File file, ModuleCandidate candidate) throws IOException {
File[] files = file.listFiles();
if (files != null)
for (File f : files)
loadSubDirectory(file, f, candidate);
}
/**
* Load a subdirectory into a module candidate. This is recursive in order to search for classfiles
* and extract their path
*/
static void loadSubDirectory(File main, File dig, ModuleCandidate candidate) {
if (dig.isDirectory()) {
File[] files = dig.listFiles();
if (files != null)
for (File f : files)
loadSubDirectory(main, f, candidate);
} else if (classFile.matcher(dig.getName()).matches()) {
Path pathCurrent = Paths.get(dig.getAbsolutePath());
Path pathMain = Paths.get(main.getAbsolutePath());
Path relative = pathMain.relativize(pathCurrent);
candidate.addClassEntry(relative.toString());
}
}
/**
* Load a Jar File as a ModuleCandidate. This will automatically parse Manifest attributes
* and other related functions for module discovery.
* @param file The file of the module.jar
* @param expandClasspath Should we expand the classpath to this jarfile if it's a valid module?
* @throws IOException
*/
static void loadJar(File file, boolean expandClasspath) throws IOException {
JarFile jar = new JarFile(file);
ModuleCandidate container = new ModuleCandidate();
boolean core = false;
Manifest mf = jar.getManifest();
if (mf != null) {
Attributes attr = mf.getMainAttributes();
if (attr != null) {
if (attr.getValue("Toast-Core-Plugin-Class") != null && isCorePhase) {
String clazz = (String) attr.getValue("Toast-Core-Plugin-Class");
container.setCorePlugin(true, clazz);
coreClasses.add(clazz);
core = true;
log.info("Injected Core Plugin: " + file.getName());
}
if (attr.getValue("Toast-Plugin-Class") != null) {
String bypassClass = (String) attr.getValue("Toast-Plugin-Class");
container.setBypass(true, bypassClass);
}
if (attr.getValue("Robot-Class") != null) {
String wrapperClazz = attr.getValue("Robot-Class");
container.setWrapper(true, wrapperClazz);
}
}
}
if (core && isCorePhase || !core && !isCorePhase) {
container.setFile(file);
for (ZipEntry ze : Collections.list(jar.entries())) {
if (classFile.matcher(ze.getName()).matches()) {
container.addClassEntry(ze.getName());
}
}
getCandidates().add(container);
if (expandClasspath)
addURL(file.toURI().toURL());
}
}
/* Searchers */
/**
* Detect any modules that are present in a development environment (-sim --search)
*/
static void loadDevEnvironment(ProfilerSection section) {
section.start("DevEnv");
for (URL url : sysLoader.getURLs()) {
try {
File f = new File(url.toURI());
if (f.isDirectory()) {
ModuleCandidate candidate = new ModuleCandidate();
loadDirectory(f, candidate);
getCandidates().add(candidate);
} else if (EnvJars.isLoadable(f)) {
loadJar(f, false);
}
} catch (Exception e) {
}
}
section.stop("DevEnv");
}
/**
* Search a directory for .jar files to load as modules. Jars will be automatically loaded, however,
* non .jar files will still be injected into the classpath.
*/
public static void search(File dir) {
File[] files = dir.listFiles(new FilenameFilter() {
@Override
public boolean accept(File dir, String name) {
return name.endsWith(".jar");
}
});
if (files != null)
for (File file : files) {
try {
loadJar(file, true);
} catch (Exception e) { }
}
File[] otherFiles = dir.listFiles(new FilenameFilter() {
@Override
public boolean accept(File dir, String name) {
return !name.endsWith(".jar");
}
});
if (otherFiles != null)
for (File file : otherFiles) {
try {
addURL(file.toURI().toURL());
} catch (Exception e) { }
}
}
/**
* Similar method to {@link #search(File)}, but will not search for Toast files, but will instead
* just add it to the load path. This is for libraries that don't load Toast, like Apache Commons,
* Language Libraries or others.
*/
public static void search_libs(File dir) {
File[] files = dir.listFiles(new FilenameFilter() {
@Override
public boolean accept(File dir, String name) {
return name.endsWith(".jar");
}
});
if (files != null)
for (File file : files) {
try {
addURL(file.toURI().toURL());
} catch (Exception e) { }
}
File[] otherFiles = dir.listFiles(new FilenameFilter() {
@Override
public boolean accept(File dir, String name) {
return !name.endsWith(".jar");
}
});
if (otherFiles != null)
for (File file : otherFiles) {
try {
addURL(file.toURI().toURL());
} catch (Exception e) { }
}
}
/* Candidation */
/**
* Start discovery for Module Candidates.
*/
private static void loadCandidates(ProfilerSection section) {
section.start("Candidate");
File[] search_dirs = new File[] { new File(ToastBootstrap.toastHome, "modules/") };
File[] lib_dirs = new File[0];
if (!isCorePhase) {
search_dirs = Storage.USB_Module("modules");
lib_dirs = Storage.USB_Module("libs");
}
for (File dir : lib_dirs) {
dir.mkdirs();
search_libs(dir);
}
for (File dir : search_dirs) {
dir.mkdirs();
search(dir);
}
section.stop("Candidate");
}
/**
* Parse the core module candidates and attempt to load them into preinit
*/
private static void parseCoreEntries(ProfilerSection section) {
section.start("Parse");
for (String clazz : coreClasses) {
try {
Class c = Class.forName(clazz);
Object object = c.newInstance();
coreObjects.add(object);
c.getDeclaredMethod("preinit").invoke(object);
} catch (Throwable e) {
}
}
section.stop("Parse");
}
/**
* Returns false if the NoLoad annotation is present, which stops 'api' classes being loaded.
*/
static boolean classLoadable(Class clazz) {
return !clazz.isAnnotationPresent(NoLoad.class);
}
/**
* Attempt to load this class as a ToastModule into the container
*/
static void parseClass(String clazz, ModuleCandidate candidate) {
try {
Class c = Class.forName(clazz);
if (ToastModule.class.isAssignableFrom(c) && classLoadable(c)) {
ModuleContainer container = new ModuleContainer(c, candidate);
getContainers().add(container);
}
} catch (Throwable e) {
}
}
/**
* Attempt to load this class as a ModuleWrapper into a container
*/
static void parseWrapper(String clazz, ModuleCandidate candidate) {
try {
Class c = Class.forName(clazz);
if (RobotBase.class.isAssignableFrom(c) && classLoadable(c) && !Toast.class.isAssignableFrom(c)) {
ModuleWrapper wrapper = new ModuleWrapper(candidate.getModuleFile(), c, candidate);
getContainers().add(new ModuleContainer(wrapper, candidate));
}
} catch (Throwable e) {
}
}
/**
* Parse non-core module candidates into their respective containers.
*/
private static void parseEntries(ProfilerSection section) {
section.start("Parse");
for (ModuleCandidate candidate : getCandidates()) {
if (candidate.isBypass()) {
parseClass(candidate.getBypassClass(), candidate);
} else if (candidate.isWrapper()) {
parseWrapper(candidate.getWrapperClass(), candidate);
} else
for (String clazz : candidate.getClassEntries()) {
parseClass(clazz, candidate);
}
candidate.freeMemory();
}
for (String clazz : manualLoadedClasses) {
ModuleCandidate candidate = new ModuleCandidate();
candidate.addClassEntry(clazz);
parseClass(clazz, candidate);
}
section.stop("Parse");
}
/* Construction */
/**
* Construct all the candidate modules. This will start parsing the names and versions of all the
* module container main classes, thus setting them up ready for prestart and start.
*/
private static void constructModules(ProfilerSection section) {
for (ModuleContainer container : getContainers()) {
try {
ProfilerEntity entity = new ProfilerEntity().start();
container.construct();
entity.stop();
entity.setName("Construct");
section.section("Module").section(container.getName()).pushEntity(entity);
log.info("Module Loaded: " + container.getDetails());
} catch (Exception e) {
}
}
}
/**
* Resolve any dependency branches on the ToastModule class. This will load classes if modules or
* class definitions are present in the Toast environment, acting as a soft dependency
* {@link jaci.openrio.toast.core.loader.annotation.Branch}
*/
private static void resolveBranches(ProfilerSection section) {
ProfilerSection section1 = section.section("Dependency");
for (ModuleContainer container : getContainers()) {
section1.start(container.getName());
container.resolve_branches();
section1.stop(container.getName());
}
}
/* Callers */
/**
* Initialize core modules by calling their init() method.
*/
public static void initCore(ProfilerSection section) {
ProfilerSection section1 = section.section("CoreJava");
section1.start("Init");
for (Object core : coreObjects) {
try {
core.getClass().getDeclaredMethod("init").invoke(core);
} catch (Throwable e) {
}
}
section1.stop("Init");
}
/**
* Post initialize core modules by calling their postinit() method.
*/
public static void postCore(ProfilerSection section) {
ProfilerSection section1 = section.section("CoreJava");
section1.start("Post");
for (Object core : coreObjects) {
try {
core.getClass().getDeclaredMethod("postinit").invoke(core);
} catch (Throwable e) {
}
}
section1.stop("Post");
}
/**
* Prestart modules in order of their {@link jaci.openrio.toast.core.loader.annotation.Priority} annotations
* (assuming the annotation is present, else treat as normal). Queued prestart methods are also invoked (dependencies)
*/
public static void prestart(ProfilerSection section) throws InvocationTargetException {
dispatch("prestart", section);
for (Method method : queuedPrestart) {
try {
method.invoke(null);
} catch (Exception e) {
Toast.log().error("Error invoking queued method: " + method.getName());
Toast.log().exception(e);
}
}
queuedPrestart.clear();
}
/**
* Start modules in order of their {@link jaci.openrio.toast.core.loader.annotation.Priority} annotations
* (assuming the annotation is present, else treat as normal).
*/
public static void start(ProfilerSection section) throws InvocationTargetException {
dispatch("start", section);
}
/* Util */
/**
* Dispatch a method to all containers
*/
public static void dispatch(String method, ProfilerSection section) throws InvocationTargetException {
if (exec == null) {
ToastModule[] mods = new ToastModule[getContainers().size()];
for (int i = 0; i < mods.length; i++) {
mods[i] = getContainers().get(i).getModule();
}
exec = new MethodExecutor(mods);
}
exec.profile(section);
exec.call(method);
}
/**
* Expand the classpath to include the provided URL
*/
public static void addURL(URL u) throws IOException {
Class sysclass = URLClassLoader.class;
try {
Method method = sysclass.getDeclaredMethod("addURL", URL.class);
method.setAccessible(true);
method.invoke(sysLoader, u);
} catch (Throwable t) { }
}
/**
* Returns true if the provided string is a valid class name and a definition is present in the classpath.
*/
public static boolean classExists(String clazz) {
try {
Class.forName(clazz);
return true;
} catch (ClassNotFoundException e) {
return false;
}
}
}