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    }