package jk_5.nailed.server.plugin; import com.google.common.collect.ImmutableSet; import com.typesafe.config.Config; import com.typesafe.config.ConfigFactory; import jk_5.nailed.api.event.plugin.RegisterAdditionalEventHandlersEvent; import jk_5.nailed.api.plugin.*; import jk_5.nailed.server.NailedPlatform; import net.minecraft.launchwrapper.Launch; import net.minecraft.launchwrapper.LaunchClassLoader; import org.apache.commons.compress.utils.IOUtils; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; import javax.annotation.Nullable; import java.io.File; import java.io.FileOutputStream; import java.io.InputStream; import java.io.InputStreamReader; import java.lang.annotation.Annotation; import java.lang.reflect.Field; import java.nio.channels.Channels; import java.nio.channels.FileChannel; import java.nio.channels.ReadableByteChannel; import java.util.Collection; import java.util.HashSet; import java.util.Set; import java.util.jar.JarFile; public class NailedPluginManager implements PluginManager { private static final NailedPluginManager INSTANCE = new NailedPluginManager(); private final Set<PluginContainer> plugins = new HashSet<PluginContainer>(); private final LaunchClassLoader classLoader = Launch.classLoader; private final Logger logger = LogManager.getLogger(); private File pluginDir; @Nullable @Override public PluginContainer getPlugin(Object identifier) { if(identifier instanceof PluginContainer){ return ((PluginContainer) identifier); }else if(identifier instanceof PluginIdentifier){ for (PluginContainer plugin : plugins) { if(plugin.getIdentifier() == identifier){ return plugin; } } }else if(identifier instanceof String){ for (PluginContainer plugin : plugins) { if(plugin.getId().equals(((String) identifier))){ return plugin; } } }else{ for (PluginContainer plugin : plugins) { if(plugin.getInstance() == identifier){ return plugin; } } } return null; } @Override public Collection<PluginContainer> getPlugins() { return ImmutableSet.copyOf(plugins); } public void loadPlugins(File dir){ this.pluginDir = dir; PluginDiscoverer.clearDiscovered(); PluginDiscoverer.discoverClasspathPlugins(); PluginDiscoverer.discoverJarPlugins(dir); Set<PluginDiscoverer.DiscoveredPlugin> discovered = PluginDiscoverer.getDiscovered(); logger.info("Discovered " + discovered.size() + " plugins"); for(PluginDiscoverer.DiscoveredPlugin d : discovered){ logger.trace(" - {} ({} version: {}) -> {}", d.id, d.name, d.version, d.className); } for (PluginDiscoverer.DiscoveredPlugin p : discovered) { try{ if(!p.isClasspath){ classLoader.addURL(p.file.toURI().toURL()); } Class<?> cl = classLoader.loadClass(p.className); Plugin annotation = cl.getAnnotation(Plugin.class); Object instance = cl.newInstance(); DefaultPluginContainer container = new DefaultPluginContainer(annotation, instance, !p.isClasspath ? p.file : null); boolean success = injectPluginIdentifiers(container) && injectConfiguration(container); if(success){ logger.info("Successfully loaded plugin " + p.name + " (id: " + p.id + " , version: " + p.version + ")"); plugins.add(container); }else{ logger.warn("Skipping plugin " + p.name); } }catch(InstantiationException e){ logger.warn("Plugin " + p.id + " (" + p.name + ", version: " + p.version + ") could not be loaded"); logger.warn(" Could not create an instance of the plugin. Is there a default constructor (no arguments)?"); logger.trace(" Full exception:", e); }catch(IllegalAccessException e){ logger.warn("Plugin ${p.id} (" + p.name + ", version: " + p.version + ") could not be loaded"); logger.warn(" The default constructor was found, but it is not public"); logger.trace(" Full exception:", e); }catch(ClassNotFoundException e){ logger.warn("Plugin " + p.id + " (" + p.name + ", version: " + p.version + ") could not be loaded"); logger.warn(" Plugin class was discovered, but does not exist. Should not be possible in a normal environment"); logger.trace(" Full exception:", e); }catch(Exception e){ logger.error("Plugin " + p.id + " (" + p.name + ", version: " + p.version + ") could not be loaded"); logger.error(" An unknown error has occurred:", e); } } for (PluginContainer p : plugins) { DefaultPluginContainer pl = (DefaultPluginContainer) p; pl.fireEvent(new RegisterAdditionalEventHandlersEvent(pl.getEventBus(), NailedPlatform.instance().getEventBus())); } } private boolean injectPluginIdentifiers(PluginContainer container){ Class<?> cl = container.getInstance().getClass(); try{ for(Field f : cl.getDeclaredFields()){ for(Annotation a : f.getDeclaredAnnotations()){ if(a.annotationType() == PluginIdentifier.Instance.class){ f.setAccessible(true); f.set(container.getInstance(), container); logger.trace("Successfully injected PluginIdentifier into " + cl.getName() + "." + f.getName()); } } } return true; }catch(Exception e){ logger.warn("Unknown exception while injecting plugin identifiers into " + container.getId(), e); return false; } } private boolean injectConfiguration(DefaultPluginContainer container){ Class<?> cl = container.getInstance().getClass(); try{ for(Field f : cl.getDeclaredFields()){ for(Annotation a : f.getDeclaredAnnotations()){ if(a.annotationType() == Configuration.class){ Configuration ann = (Configuration) a; File pluginDir = new File(this.pluginDir, container.getId()); Config config; try{ config = initPluginConfig(container, new File(pluginDir, ann.filename()), ann.defaults()); }catch(RuntimeException e){ logger.error(""); logger.error("Error while injecting plugin config for " + container.getName() + ":"); logger.error(e.getMessage()); logger.error("This plugin will not be loaded!"); logger.error(""); config = null; } if(config == null){ return false; } f.setAccessible(true); f.set(container.getInstance(), config); logger.trace("Successfully injected plugin config into " + cl.getName() + "." + f.getName()); } } } return true; }catch(Exception e){ logger.warn("Unknown exception while injecting plugin configuration into " + container.getId(), e); return false; } } public void enablePlugins(){ } private Config initPluginConfig(DefaultPluginContainer container, File configLocation, String defaultPath){ InputStream defaultInput; if(container.hasLocation()){ defaultInput = getConfigFromJar(container.getLocation(), defaultPath); }else{ defaultInput = NailedPluginManager.class.getResourceAsStream("/" + defaultPath); } try{ if(defaultInput == null){ throw new RuntimeException("Default configuration path " + defaultPath + " could not be found"); } if(!configLocation.exists() || configLocation.length() == 0){ configLocation.getParentFile().mkdirs(); ReadableByteChannel inChannel = Channels.newChannel(defaultInput); FileChannel out = new FileOutputStream(configLocation).getChannel(); out.transferFrom(inChannel, 0, Long.MAX_VALUE); out.close(); } if(container.hasLocation()){ defaultInput = getConfigFromJar(container.getLocation(), defaultPath); }else{ defaultInput = NailedPluginManager.class.getResourceAsStream("/" + defaultPath); } Config defaults = ConfigFactory.parseReader(new InputStreamReader(defaultInput)); return ConfigFactory.parseFile(configLocation).withFallback(defaults); }catch(Exception e){ }finally{ IOUtils.closeQuietly(defaultInput); } return null; } private InputStream getConfigFromJar(File jar, String entry){ try{ JarFile jarFile = new JarFile(jar); return jarFile.getInputStream(jarFile.getEntry(entry)); }catch(Exception e){ return null; } } public static NailedPluginManager instance(){ return INSTANCE; } }