001 package org.bukkit.plugin.java;
002
003 import java.io.File;
004 import java.io.FileNotFoundException;
005 import java.io.IOException;
006 import java.io.InputStream;
007 import java.lang.reflect.InvocationTargetException;
008 import java.lang.reflect.Method;
009 import java.util.Arrays;
010 import java.util.HashMap;
011 import java.util.HashSet;
012 import java.util.LinkedHashMap;
013 import java.util.Map;
014 import java.util.Set;
015 import java.util.jar.JarEntry;
016 import java.util.jar.JarFile;
017 import java.util.logging.Level;
018 import java.util.regex.Pattern;
019
020 import org.apache.commons.lang.Validate;
021 import org.bukkit.Server;
022 import org.bukkit.Warning;
023 import org.bukkit.Warning.WarningState;
024 import org.bukkit.configuration.serialization.ConfigurationSerializable;
025 import org.bukkit.configuration.serialization.ConfigurationSerialization;
026 import org.bukkit.event.Event;
027 import org.bukkit.event.EventException;
028 import org.bukkit.event.EventHandler;
029 import org.bukkit.event.Listener;
030 import org.bukkit.event.server.PluginDisableEvent;
031 import org.bukkit.event.server.PluginEnableEvent;
032 import org.bukkit.plugin.AuthorNagException;
033 import org.bukkit.plugin.EventExecutor;
034 import org.bukkit.plugin.InvalidDescriptionException;
035 import org.bukkit.plugin.InvalidPluginException;
036 import org.bukkit.plugin.Plugin;
037 import org.bukkit.plugin.PluginDescriptionFile;
038 import org.bukkit.plugin.PluginLoader;
039 import org.bukkit.plugin.RegisteredListener;
040 import org.bukkit.plugin.TimedRegisteredListener;
041 import org.bukkit.plugin.UnknownDependencyException;
042 import org.yaml.snakeyaml.error.YAMLException;
043
044 /**
045 * Represents a Java plugin loader, allowing plugins in the form of .jar
046 */
047 public final class JavaPluginLoader implements PluginLoader {
048 final Server server;
049 private final Pattern[] fileFilters = new Pattern[] { Pattern.compile("\\.jar$"), };
050 private final Map<String, Class<?>> classes = new HashMap<String, Class<?>>();
051 private final Map<String, PluginClassLoader> loaders = new LinkedHashMap<String, PluginClassLoader>();
052
053 /**
054 * This class was not meant to be constructed explicitly
055 */
056 @Deprecated
057 public JavaPluginLoader(Server instance) {
058 Validate.notNull(instance, "Server cannot be null");
059 server = instance;
060 }
061
062 public Plugin loadPlugin(final File file) throws InvalidPluginException {
063 Validate.notNull(file, "File cannot be null");
064
065 if (!file.exists()) {
066 throw new InvalidPluginException(new FileNotFoundException(file.getPath() + " does not exist"));
067 }
068
069 final PluginDescriptionFile description;
070 try {
071 description = getPluginDescription(file);
072 } catch (InvalidDescriptionException ex) {
073 throw new InvalidPluginException(ex);
074 }
075
076 final File parentFile = file.getParentFile();
077 final File dataFolder = new File(parentFile, description.getName());
078 @SuppressWarnings("deprecation")
079 final File oldDataFolder = new File(parentFile, description.getRawName());
080
081 // Found old data folder
082 if (dataFolder.equals(oldDataFolder)) {
083 // They are equal -- nothing needs to be done!
084 } else if (dataFolder.isDirectory() && oldDataFolder.isDirectory()) {
085 server.getLogger().warning(String.format(
086 "While loading %s (%s) found old-data folder: `%s' next to the new one `%s'",
087 description.getFullName(),
088 file,
089 oldDataFolder,
090 dataFolder
091 ));
092 } else if (oldDataFolder.isDirectory() && !dataFolder.exists()) {
093 if (!oldDataFolder.renameTo(dataFolder)) {
094 throw new InvalidPluginException("Unable to rename old data folder: `" + oldDataFolder + "' to: `" + dataFolder + "'");
095 }
096 server.getLogger().log(Level.INFO, String.format(
097 "While loading %s (%s) renamed data folder: `%s' to `%s'",
098 description.getFullName(),
099 file,
100 oldDataFolder,
101 dataFolder
102 ));
103 }
104
105 if (dataFolder.exists() && !dataFolder.isDirectory()) {
106 throw new InvalidPluginException(String.format(
107 "Projected datafolder: `%s' for %s (%s) exists and is not a directory",
108 dataFolder,
109 description.getFullName(),
110 file
111 ));
112 }
113
114 for (final String pluginName : description.getDepend()) {
115 if (loaders == null) {
116 throw new UnknownDependencyException(pluginName);
117 }
118 PluginClassLoader current = loaders.get(pluginName);
119
120 if (current == null) {
121 throw new UnknownDependencyException(pluginName);
122 }
123 }
124
125 final PluginClassLoader loader;
126 try {
127 loader = new PluginClassLoader(this, getClass().getClassLoader(), description, dataFolder, file);
128 } catch (InvalidPluginException ex) {
129 throw ex;
130 } catch (Throwable ex) {
131 throw new InvalidPluginException(ex);
132 }
133
134 loaders.put(description.getName(), loader);
135
136 return loader.plugin;
137 }
138
139 public PluginDescriptionFile getPluginDescription(File file) throws InvalidDescriptionException {
140 Validate.notNull(file, "File cannot be null");
141
142 JarFile jar = null;
143 InputStream stream = null;
144
145 try {
146 jar = new JarFile(file);
147 JarEntry entry = jar.getJarEntry("plugin.yml");
148
149 if (entry == null) {
150 throw new InvalidDescriptionException(new FileNotFoundException("Jar does not contain plugin.yml"));
151 }
152
153 stream = jar.getInputStream(entry);
154
155 return new PluginDescriptionFile(stream);
156
157 } catch (IOException ex) {
158 throw new InvalidDescriptionException(ex);
159 } catch (YAMLException ex) {
160 throw new InvalidDescriptionException(ex);
161 } finally {
162 if (jar != null) {
163 try {
164 jar.close();
165 } catch (IOException e) {
166 }
167 }
168 if (stream != null) {
169 try {
170 stream.close();
171 } catch (IOException e) {
172 }
173 }
174 }
175 }
176
177 public Pattern[] getPluginFileFilters() {
178 return fileFilters.clone();
179 }
180
181 Class<?> getClassByName(final String name) {
182 Class<?> cachedClass = classes.get(name);
183
184 if (cachedClass != null) {
185 return cachedClass;
186 } else {
187 for (String current : loaders.keySet()) {
188 PluginClassLoader loader = loaders.get(current);
189
190 try {
191 cachedClass = loader.findClass(name, false);
192 } catch (ClassNotFoundException cnfe) {}
193 if (cachedClass != null) {
194 return cachedClass;
195 }
196 }
197 }
198 return null;
199 }
200
201 void setClass(final String name, final Class<?> clazz) {
202 if (!classes.containsKey(name)) {
203 classes.put(name, clazz);
204
205 if (ConfigurationSerializable.class.isAssignableFrom(clazz)) {
206 Class<? extends ConfigurationSerializable> serializable = clazz.asSubclass(ConfigurationSerializable.class);
207 ConfigurationSerialization.registerClass(serializable);
208 }
209 }
210 }
211
212 private void removeClass(String name) {
213 Class<?> clazz = classes.remove(name);
214
215 try {
216 if ((clazz != null) && (ConfigurationSerializable.class.isAssignableFrom(clazz))) {
217 Class<? extends ConfigurationSerializable> serializable = clazz.asSubclass(ConfigurationSerializable.class);
218 ConfigurationSerialization.unregisterClass(serializable);
219 }
220 } catch (NullPointerException ex) {
221 // Boggle!
222 // (Native methods throwing NPEs is not fun when you can't stop it before-hand)
223 }
224 }
225
226 public Map<Class<? extends Event>, Set<RegisteredListener>> createRegisteredListeners(Listener listener, final Plugin plugin) {
227 Validate.notNull(plugin, "Plugin can not be null");
228 Validate.notNull(listener, "Listener can not be null");
229
230 boolean useTimings = server.getPluginManager().useTimings();
231 Map<Class<? extends Event>, Set<RegisteredListener>> ret = new HashMap<Class<? extends Event>, Set<RegisteredListener>>();
232 Set<Method> methods;
233 try {
234 Method[] publicMethods = listener.getClass().getMethods();
235 methods = new HashSet<Method>(publicMethods.length, Float.MAX_VALUE);
236 for (Method method : publicMethods) {
237 methods.add(method);
238 }
239 for (Method method : listener.getClass().getDeclaredMethods()) {
240 methods.add(method);
241 }
242 } catch (NoClassDefFoundError e) {
243 plugin.getLogger().severe("Plugin " + plugin.getDescription().getFullName() + " has failed to register events for " + listener.getClass() + " because " + e.getMessage() + " does not exist.");
244 return ret;
245 }
246
247 for (final Method method : methods) {
248 final EventHandler eh = method.getAnnotation(EventHandler.class);
249 if (eh == null) continue;
250 final Class<?> checkClass;
251 if (method.getParameterTypes().length != 1 || !Event.class.isAssignableFrom(checkClass = method.getParameterTypes()[0])) {
252 plugin.getLogger().severe(plugin.getDescription().getFullName() + " attempted to register an invalid EventHandler method signature \"" + method.toGenericString() + "\" in " + listener.getClass());
253 continue;
254 }
255 final Class<? extends Event> eventClass = checkClass.asSubclass(Event.class);
256 method.setAccessible(true);
257 Set<RegisteredListener> eventSet = ret.get(eventClass);
258 if (eventSet == null) {
259 eventSet = new HashSet<RegisteredListener>();
260 ret.put(eventClass, eventSet);
261 }
262
263 for (Class<?> clazz = eventClass; Event.class.isAssignableFrom(clazz); clazz = clazz.getSuperclass()) {
264 // This loop checks for extending deprecated events
265 if (clazz.getAnnotation(Deprecated.class) != null) {
266 Warning warning = clazz.getAnnotation(Warning.class);
267 WarningState warningState = server.getWarningState();
268 if (!warningState.printFor(warning)) {
269 break;
270 }
271 plugin.getLogger().log(
272 Level.WARNING,
273 String.format(
274 "\"%s\" has registered a listener for %s on method \"%s\", but the event is Deprecated." +
275 " \"%s\"; please notify the authors %s.",
276 plugin.getDescription().getFullName(),
277 clazz.getName(),
278 method.toGenericString(),
279 (warning != null && warning.reason().length() != 0) ? warning.reason() : "Server performance will be affected",
280 Arrays.toString(plugin.getDescription().getAuthors().toArray())),
281 warningState == WarningState.ON ? new AuthorNagException(null) : null);
282 break;
283 }
284 }
285
286 EventExecutor executor = new EventExecutor() {
287 public void execute(Listener listener, Event event) throws EventException {
288 try {
289 if (!eventClass.isAssignableFrom(event.getClass())) {
290 return;
291 }
292 method.invoke(listener, event);
293 } catch (InvocationTargetException ex) {
294 throw new EventException(ex.getCause());
295 } catch (Throwable t) {
296 throw new EventException(t);
297 }
298 }
299 };
300 if (useTimings) {
301 eventSet.add(new TimedRegisteredListener(listener, executor, eh.priority(), plugin, eh.ignoreCancelled()));
302 } else {
303 eventSet.add(new RegisteredListener(listener, executor, eh.priority(), plugin, eh.ignoreCancelled()));
304 }
305 }
306 return ret;
307 }
308
309 public void enablePlugin(final Plugin plugin) {
310 Validate.isTrue(plugin instanceof JavaPlugin, "Plugin is not associated with this PluginLoader");
311
312 if (!plugin.isEnabled()) {
313 plugin.getLogger().info("Enabling " + plugin.getDescription().getFullName());
314
315 JavaPlugin jPlugin = (JavaPlugin) plugin;
316
317 String pluginName = jPlugin.getDescription().getName();
318
319 if (!loaders.containsKey(pluginName)) {
320 loaders.put(pluginName, (PluginClassLoader) jPlugin.getClassLoader());
321 }
322
323 try {
324 jPlugin.setEnabled(true);
325 } catch (Throwable ex) {
326 server.getLogger().log(Level.SEVERE, "Error occurred while enabling " + plugin.getDescription().getFullName() + " (Is it up to date?)", ex);
327 }
328
329 // Perhaps abort here, rather than continue going, but as it stands,
330 // an abort is not possible the way it's currently written
331 server.getPluginManager().callEvent(new PluginEnableEvent(plugin));
332 }
333 }
334
335 public void disablePlugin(Plugin plugin) {
336 Validate.isTrue(plugin instanceof JavaPlugin, "Plugin is not associated with this PluginLoader");
337
338 if (plugin.isEnabled()) {
339 String message = String.format("Disabling %s", plugin.getDescription().getFullName());
340 plugin.getLogger().info(message);
341
342 server.getPluginManager().callEvent(new PluginDisableEvent(plugin));
343
344 JavaPlugin jPlugin = (JavaPlugin) plugin;
345 ClassLoader cloader = jPlugin.getClassLoader();
346
347 try {
348 jPlugin.setEnabled(false);
349 } catch (Throwable ex) {
350 server.getLogger().log(Level.SEVERE, "Error occurred while disabling " + plugin.getDescription().getFullName() + " (Is it up to date?)", ex);
351 }
352
353 loaders.remove(jPlugin.getDescription().getName());
354
355 if (cloader instanceof PluginClassLoader) {
356 PluginClassLoader loader = (PluginClassLoader) cloader;
357 Set<String> names = loader.getClasses();
358
359 for (String name : names) {
360 removeClass(name);
361 }
362 }
363 }
364 }
365 }