package org.bukkit.plugin.java; import java.io.File; import java.io.FileNotFoundException; import java.io.IOException; import java.io.InputStream; import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; import java.util.Arrays; import java.util.HashMap; import java.util.HashSet; import java.util.LinkedHashMap; import java.util.Map; import java.util.Set; import java.util.jar.JarEntry; import java.util.jar.JarFile; import java.util.logging.Level; import java.util.regex.Pattern; import org.apache.commons.lang.Validate; import org.bukkit.Server; import org.bukkit.Warning; import org.bukkit.Warning.WarningState; import org.bukkit.configuration.serialization.ConfigurationSerializable; import org.bukkit.configuration.serialization.ConfigurationSerialization; import org.bukkit.event.Event; import org.bukkit.event.EventException; import org.bukkit.event.EventHandler; import org.bukkit.event.Listener; import org.bukkit.event.server.PluginDisableEvent; import org.bukkit.event.server.PluginEnableEvent; import org.bukkit.plugin.AuthorNagException; import org.bukkit.plugin.EventExecutor; import org.bukkit.plugin.InvalidDescriptionException; import org.bukkit.plugin.InvalidPluginException; import org.bukkit.plugin.Plugin; import org.bukkit.plugin.PluginDescriptionFile; import org.bukkit.plugin.PluginLoader; import org.bukkit.plugin.RegisteredListener; import org.bukkit.plugin.TimedRegisteredListener; import org.bukkit.plugin.UnknownDependencyException; import org.yaml.snakeyaml.error.YAMLException; /** * Represents a Java plugin loader, allowing plugins in the form of .jar */ public final class JavaPluginLoader implements PluginLoader { final Server server; private final Pattern[] fileFilters = new Pattern[] { Pattern.compile("\\.jar$"), }; private final Map<String, Class<?>> classes = new HashMap<String, Class<?>>(); private final Map<String, PluginClassLoader> loaders = new LinkedHashMap<String, PluginClassLoader>(); /** * This class was not meant to be constructed explicitly */ @Deprecated public JavaPluginLoader(Server instance) { Validate.notNull(instance, "Server cannot be null"); server = instance; } public Plugin loadPlugin(final File file) throws InvalidPluginException { Validate.notNull(file, "File cannot be null"); if (!file.exists()) { throw new InvalidPluginException(new FileNotFoundException(file.getPath() + " does not exist")); } final PluginDescriptionFile description; try { description = getPluginDescription(file); } catch (InvalidDescriptionException ex) { throw new InvalidPluginException(ex); } final File parentFile = file.getParentFile(); final File dataFolder = new File(parentFile, description.getName()); @SuppressWarnings("deprecation") final File oldDataFolder = new File(parentFile, description.getRawName()); // Found old data folder if (dataFolder.equals(oldDataFolder)) { // They are equal -- nothing needs to be done! } else if (dataFolder.isDirectory() && oldDataFolder.isDirectory()) { server.getLogger().warning(String.format( "While loading %s (%s) found old-data folder: `%s' next to the new one `%s'", description.getFullName(), file, oldDataFolder, dataFolder )); } else if (oldDataFolder.isDirectory() && !dataFolder.exists()) { if (!oldDataFolder.renameTo(dataFolder)) { throw new InvalidPluginException("Unable to rename old data folder: `" + oldDataFolder + "' to: `" + dataFolder + "'"); } server.getLogger().log(Level.INFO, String.format( "While loading %s (%s) renamed data folder: `%s' to `%s'", description.getFullName(), file, oldDataFolder, dataFolder )); } if (dataFolder.exists() && !dataFolder.isDirectory()) { throw new InvalidPluginException(String.format( "Projected datafolder: `%s' for %s (%s) exists and is not a directory", dataFolder, description.getFullName(), file )); } for (final String pluginName : description.getDepend()) { if (loaders == null) { throw new UnknownDependencyException(pluginName); } PluginClassLoader current = loaders.get(pluginName); if (current == null) { throw new UnknownDependencyException(pluginName); } } final PluginClassLoader loader; try { loader = new PluginClassLoader(this, getClass().getClassLoader(), description, dataFolder, file); } catch (InvalidPluginException ex) { throw ex; } catch (Throwable ex) { throw new InvalidPluginException(ex); } loaders.put(description.getName(), loader); return loader.plugin; } public PluginDescriptionFile getPluginDescription(File file) throws InvalidDescriptionException { Validate.notNull(file, "File cannot be null"); JarFile jar = null; InputStream stream = null; try { jar = new JarFile(file); JarEntry entry = jar.getJarEntry("plugin.yml"); if (entry == null) { throw new InvalidDescriptionException(new FileNotFoundException("Jar does not contain plugin.yml")); } stream = jar.getInputStream(entry); return new PluginDescriptionFile(stream); } catch (IOException ex) { throw new InvalidDescriptionException(ex); } catch (YAMLException ex) { throw new InvalidDescriptionException(ex); } finally { if (jar != null) { try { jar.close(); } catch (IOException e) { } } if (stream != null) { try { stream.close(); } catch (IOException e) { } } } } public Pattern[] getPluginFileFilters() { return fileFilters.clone(); } Class<?> getClassByName(final String name) { Class<?> cachedClass = classes.get(name); if (cachedClass != null) { return cachedClass; } else { for (String current : loaders.keySet()) { PluginClassLoader loader = loaders.get(current); try { cachedClass = loader.findClass(name, false); } catch (ClassNotFoundException cnfe) {} if (cachedClass != null) { return cachedClass; } } } return null; } void setClass(final String name, final Class<?> clazz) { if (!classes.containsKey(name)) { classes.put(name, clazz); if (ConfigurationSerializable.class.isAssignableFrom(clazz)) { Class<? extends ConfigurationSerializable> serializable = clazz.asSubclass(ConfigurationSerializable.class); ConfigurationSerialization.registerClass(serializable); } } } private void removeClass(String name) { Class<?> clazz = classes.remove(name); try { if ((clazz != null) && (ConfigurationSerializable.class.isAssignableFrom(clazz))) { Class<? extends ConfigurationSerializable> serializable = clazz.asSubclass(ConfigurationSerializable.class); ConfigurationSerialization.unregisterClass(serializable); } } catch (NullPointerException ex) { // Boggle! // (Native methods throwing NPEs is not fun when you can't stop it before-hand) } } public Map<Class<? extends Event>, Set<RegisteredListener>> createRegisteredListeners(Listener listener, final Plugin plugin) { Validate.notNull(plugin, "Plugin can not be null"); Validate.notNull(listener, "Listener can not be null"); boolean useTimings = server.getPluginManager().useTimings(); Map<Class<? extends Event>, Set<RegisteredListener>> ret = new HashMap<Class<? extends Event>, Set<RegisteredListener>>(); Set<Method> methods; try { Method[] publicMethods = listener.getClass().getMethods(); methods = new HashSet<Method>(publicMethods.length, Float.MAX_VALUE); for (Method method : publicMethods) { methods.add(method); } for (Method method : listener.getClass().getDeclaredMethods()) { methods.add(method); } } catch (NoClassDefFoundError e) { plugin.getLogger().severe("Plugin " + plugin.getDescription().getFullName() + " has failed to register events for " + listener.getClass() + " because " + e.getMessage() + " does not exist."); return ret; } for (final Method method : methods) { final EventHandler eh = method.getAnnotation(EventHandler.class); if (eh == null) continue; final Class<?> checkClass; if (method.getParameterTypes().length != 1 || !Event.class.isAssignableFrom(checkClass = method.getParameterTypes()[0])) { plugin.getLogger().severe(plugin.getDescription().getFullName() + " attempted to register an invalid EventHandler method signature \"" + method.toGenericString() + "\" in " + listener.getClass()); continue; } final Class<? extends Event> eventClass = checkClass.asSubclass(Event.class); method.setAccessible(true); Set<RegisteredListener> eventSet = ret.get(eventClass); if (eventSet == null) { eventSet = new HashSet<RegisteredListener>(); ret.put(eventClass, eventSet); } for (Class<?> clazz = eventClass; Event.class.isAssignableFrom(clazz); clazz = clazz.getSuperclass()) { // This loop checks for extending deprecated events if (clazz.getAnnotation(Deprecated.class) != null) { Warning warning = clazz.getAnnotation(Warning.class); WarningState warningState = server.getWarningState(); if (!warningState.printFor(warning)) { break; } plugin.getLogger().log( Level.WARNING, String.format( "\"%s\" has registered a listener for %s on method \"%s\", but the event is Deprecated." + " \"%s\"; please notify the authors %s.", plugin.getDescription().getFullName(), clazz.getName(), method.toGenericString(), (warning != null && warning.reason().length() != 0) ? warning.reason() : "Server performance will be affected", Arrays.toString(plugin.getDescription().getAuthors().toArray())), warningState == WarningState.ON ? new AuthorNagException(null) : null); break; } } EventExecutor executor = new EventExecutor() { public void execute(Listener listener, Event event) throws EventException { try { if (!eventClass.isAssignableFrom(event.getClass())) { return; } method.invoke(listener, event); } catch (InvocationTargetException ex) { throw new EventException(ex.getCause()); } catch (Throwable t) { throw new EventException(t); } } }; if (useTimings) { eventSet.add(new TimedRegisteredListener(listener, executor, eh.priority(), plugin, eh.ignoreCancelled())); } else { eventSet.add(new RegisteredListener(listener, executor, eh.priority(), plugin, eh.ignoreCancelled())); } } return ret; } public void enablePlugin(final Plugin plugin) { Validate.isTrue(plugin instanceof JavaPlugin, "Plugin is not associated with this PluginLoader"); if (!plugin.isEnabled()) { plugin.getLogger().info("Enabling " + plugin.getDescription().getFullName()); JavaPlugin jPlugin = (JavaPlugin) plugin; String pluginName = jPlugin.getDescription().getName(); if (!loaders.containsKey(pluginName)) { loaders.put(pluginName, (PluginClassLoader) jPlugin.getClassLoader()); } try { jPlugin.setEnabled(true); } catch (Throwable ex) { server.getLogger().log(Level.SEVERE, "Error occurred while enabling " + plugin.getDescription().getFullName() + " (Is it up to date?)", ex); } // Perhaps abort here, rather than continue going, but as it stands, // an abort is not possible the way it's currently written server.getPluginManager().callEvent(new PluginEnableEvent(plugin)); } } public void disablePlugin(Plugin plugin) { Validate.isTrue(plugin instanceof JavaPlugin, "Plugin is not associated with this PluginLoader"); if (plugin.isEnabled()) { String message = String.format("Disabling %s", plugin.getDescription().getFullName()); plugin.getLogger().info(message); server.getPluginManager().callEvent(new PluginDisableEvent(plugin)); JavaPlugin jPlugin = (JavaPlugin) plugin; ClassLoader cloader = jPlugin.getClassLoader(); try { jPlugin.setEnabled(false); } catch (Throwable ex) { server.getLogger().log(Level.SEVERE, "Error occurred while disabling " + plugin.getDescription().getFullName() + " (Is it up to date?)", ex); } loaders.remove(jPlugin.getDescription().getName()); if (cloader instanceof PluginClassLoader) { PluginClassLoader loader = (PluginClassLoader) cloader; Set<String> names = loader.getClasses(); for (String name : names) { removeClass(name); } } } } }