package moe.nea.firmament.init; import java.io.File; import java.io.IOException; import java.net.MalformedURLException; import java.net.URISyntaxException; import java.net.URL; import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.Paths; import java.util.ArrayList; import java.util.List; import java.util.stream.Stream; import java.util.zip.ZipEntry; import java.util.zip.ZipInputStream; public class AutoDiscoveryPlugin { private static final List mixinPlugins = new ArrayList<>(); public static List getMixinPlugins() { return mixinPlugins; } private String mixinPackage; public void setMixinPackage(String mixinPackage) { this.mixinPackage = mixinPackage; mixinPlugins.add(this); } /** * Resolves the base class root for a given class URL. This resolves either the JAR root, or the class file root. * In either case the return value of this + the class name will resolve back to the original class url, or to other * class urls for other classes. */ public URL getBaseUrlForClassUrl(URL classUrl) { String string = classUrl.toString(); if (classUrl.getProtocol().equals("jar")) { try { return new URL(string.substring(4).split("!")[0]); } catch (MalformedURLException e) { throw new RuntimeException(e); } } if (string.endsWith(".class")) { try { return new URL(string.replace("\\", "/") .replace(getClass().getCanonicalName() .replace(".", "/") + ".class", "")); } catch (MalformedURLException e) { throw new RuntimeException(e); } } return classUrl; } /** * Get the package that contains all the mixins. This value is set using {@link #setMixinPackage}. */ public String getMixinPackage() { return mixinPackage; } /** * Get the path inside the class root to the mixin package */ public String getMixinBaseDir() { return mixinPackage.replace(".", "/"); } /** * A list of all discovered mixins. */ private List mixins = null; /** * Try to add mixin class ot the mixins based on the filepath inside of the class root. * Removes the {@code .class} file suffix, as well as the base mixin package. *

This method cannot be called after mixin initialization.

* * @param className the name or path of a class to be registered as a mixin. */ public void tryAddMixinClass(String className) { if (!className.endsWith(".class")) return; String norm = (className.substring(0, className.length() - ".class".length())) .replace("\\", "/") .replace("/", "."); if (norm.startsWith(getMixinPackage() + ".") && !norm.endsWith(".")) { mixins.add(norm.substring(getMixinPackage().length() + 1)); } } private void tryDiscoverFromContentFile(URL url) { Path file; try { file = Paths.get(getBaseUrlForClassUrl(url).toURI()); } catch (URISyntaxException e) { throw new RuntimeException(e); } System.out.println("Base directory found at " + file); if (!Files.exists(file)) { System.out.println("Skipping non-existing mixin root: " + file); return; } if (Files.isDirectory(file)) { walkDir(file); } else { walkJar(file); } System.out.println("Found mixins: " + mixins); } /** * Search through the JAR or class directory to find mixins contained in {@link #getMixinPackage()} */ public List getMixins() { if (mixins != null) return mixins; System.out.println("Trying to discover mixins"); mixins = new ArrayList<>(); URL classUrl = getClass().getProtectionDomain().getCodeSource().getLocation(); System.out.println("Found classes at " + classUrl); tryDiscoverFromContentFile(classUrl); var classRoots = System.getProperty("firmament.classroots"); if (classRoots != null && !classRoots.isBlank()) { System.out.println("Found firmament class roots: " + classRoots); for (String s : classRoots.split(File.pathSeparator)) { if (s.isBlank()) { continue; } try { tryDiscoverFromContentFile(new File(s).toURI().toURL()); } catch (MalformedURLException e) { throw new RuntimeException(e); } } } return mixins; } /** * Search through directory for mixin classes based on {@link #getMixinBaseDir}. * * @param classRoot The root directory in which classes are stored for the default package. */ private void walkDir(Path classRoot) { System.out.println("Trying to find mixins from directory"); try (Stream classes = Files.walk(classRoot.resolve(getMixinBaseDir()))) { classes.map(it -> classRoot.relativize(it).toString()) .forEach(this::tryAddMixinClass); } catch (IOException e) { throw new RuntimeException(e); } } /** * Read through a JAR file, trying to find all mixins inside. */ private void walkJar(Path file) { System.out.println("Trying to find mixins from jar file"); try (ZipInputStream zis = new ZipInputStream(Files.newInputStream(file))) { ZipEntry next; while ((next = zis.getNextEntry()) != null) { tryAddMixinClass(next.getName()); zis.closeEntry(); } } catch (IOException e) { throw new RuntimeException(e); } } }