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 }