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);
}
}
}
}
}