001 package org.bukkit.plugin.java; 002 003 import java.io.File; 004 import java.io.FileOutputStream; 005 import java.io.IOException; 006 import java.io.InputStream; 007 import java.io.InputStreamReader; 008 import java.io.OutputStream; 009 import java.io.Reader; 010 import java.net.URL; 011 import java.net.URLConnection; 012 import java.nio.charset.Charset; 013 import java.util.ArrayList; 014 import java.util.List; 015 import java.util.logging.Level; 016 import java.util.logging.Logger; 017 018 import org.apache.commons.lang.Validate; 019 import org.bukkit.Server; 020 import org.bukkit.Warning.WarningState; 021 import org.bukkit.command.Command; 022 import org.bukkit.command.CommandSender; 023 import org.bukkit.command.PluginCommand; 024 import org.bukkit.configuration.InvalidConfigurationException; 025 import org.bukkit.configuration.file.FileConfiguration; 026 import org.bukkit.configuration.file.YamlConfiguration; 027 import org.bukkit.generator.ChunkGenerator; 028 import org.bukkit.plugin.AuthorNagException; 029 import org.bukkit.plugin.PluginAwareness; 030 import org.bukkit.plugin.PluginBase; 031 import org.bukkit.plugin.PluginDescriptionFile; 032 import org.bukkit.plugin.PluginLoader; 033 import org.bukkit.plugin.PluginLogger; 034 035 import com.avaje.ebean.EbeanServer; 036 import com.avaje.ebean.EbeanServerFactory; 037 import com.avaje.ebean.config.DataSourceConfig; 038 import com.avaje.ebean.config.ServerConfig; 039 import com.avaje.ebeaninternal.api.SpiEbeanServer; 040 import com.avaje.ebeaninternal.server.ddl.DdlGenerator; 041 import com.google.common.base.Charsets; 042 import com.google.common.io.ByteStreams; 043 044 /** 045 * Represents a Java plugin 046 */ 047 public abstract class JavaPlugin extends PluginBase { 048 private boolean isEnabled = false; 049 private PluginLoader loader = null; 050 private Server server = null; 051 private File file = null; 052 private PluginDescriptionFile description = null; 053 private File dataFolder = null; 054 private ClassLoader classLoader = null; 055 private boolean naggable = true; 056 private EbeanServer ebean = null; 057 private FileConfiguration newConfig = null; 058 private File configFile = null; 059 private PluginLogger logger = null; 060 061 public JavaPlugin() { 062 final ClassLoader classLoader = this.getClass().getClassLoader(); 063 if (!(classLoader instanceof PluginClassLoader)) { 064 throw new IllegalStateException("JavaPlugin requires " + PluginClassLoader.class.getName()); 065 } 066 ((PluginClassLoader) classLoader).initialize(this); 067 } 068 069 /** 070 * @deprecated This method is intended for unit testing purposes when the 071 * other {@linkplain #JavaPlugin(JavaPluginLoader, 072 * PluginDescriptionFile, File, File) constructor} cannot be used. 073 * <p> 074 * Its existence may be temporary. 075 */ 076 @Deprecated 077 protected JavaPlugin(final PluginLoader loader, final Server server, final PluginDescriptionFile description, final File dataFolder, final File file) { 078 final ClassLoader classLoader = this.getClass().getClassLoader(); 079 if (classLoader instanceof PluginClassLoader) { 080 throw new IllegalStateException("Cannot use initialization constructor at runtime"); 081 } 082 init(loader, server, description, dataFolder, file, classLoader); 083 } 084 085 protected JavaPlugin(final JavaPluginLoader loader, final PluginDescriptionFile description, final File dataFolder, final File file) { 086 final ClassLoader classLoader = this.getClass().getClassLoader(); 087 if (classLoader instanceof PluginClassLoader) { 088 throw new IllegalStateException("Cannot use initialization constructor at runtime"); 089 } 090 init(loader, loader.server, description, dataFolder, file, classLoader); 091 } 092 093 /** 094 * Returns the folder that the plugin data's files are located in. The 095 * folder may not yet exist. 096 * 097 * @return The folder. 098 */ 099 @Override 100 public final File getDataFolder() { 101 return dataFolder; 102 } 103 104 /** 105 * Gets the associated PluginLoader responsible for this plugin 106 * 107 * @return PluginLoader that controls this plugin 108 */ 109 @Override 110 public final PluginLoader getPluginLoader() { 111 return loader; 112 } 113 114 /** 115 * Returns the Server instance currently running this plugin 116 * 117 * @return Server running this plugin 118 */ 119 @Override 120 public final Server getServer() { 121 return server; 122 } 123 124 /** 125 * Returns a value indicating whether or not this plugin is currently 126 * enabled 127 * 128 * @return true if this plugin is enabled, otherwise false 129 */ 130 @Override 131 public final boolean isEnabled() { 132 return isEnabled; 133 } 134 135 /** 136 * Returns the file which contains this plugin 137 * 138 * @return File containing this plugin 139 */ 140 protected File getFile() { 141 return file; 142 } 143 144 /** 145 * Returns the plugin.yaml file containing the details for this plugin 146 * 147 * @return Contents of the plugin.yaml file 148 */ 149 @Override 150 public final PluginDescriptionFile getDescription() { 151 return description; 152 } 153 154 @Override 155 public FileConfiguration getConfig() { 156 if (newConfig == null) { 157 reloadConfig(); 158 } 159 return newConfig; 160 } 161 162 /** 163 * Provides a reader for a text file located inside the jar. The behavior 164 * of this method adheres to {@link PluginAwareness.Flags#UTF8}, or if not 165 * defined, uses UTF8 if {@link FileConfiguration#UTF8_OVERRIDE} is 166 * specified, or system default otherwise. 167 * 168 * @param file the filename of the resource to load 169 * @return null if {@link #getResource(String)} returns null 170 * @throws IllegalArgumentException if file is null 171 * @see ClassLoader#getResourceAsStream(String) 172 */ 173 @SuppressWarnings("deprecation") 174 protected final Reader getTextResource(String file) { 175 final InputStream in = getResource(file); 176 177 return in == null ? null : new InputStreamReader(in, isStrictlyUTF8() || FileConfiguration.UTF8_OVERRIDE ? Charsets.UTF_8 : Charset.defaultCharset()); 178 } 179 180 @SuppressWarnings("deprecation") 181 @Override 182 public void reloadConfig() { 183 newConfig = YamlConfiguration.loadConfiguration(configFile); 184 185 final InputStream defConfigStream = getResource("config.yml"); 186 if (defConfigStream == null) { 187 return; 188 } 189 190 final YamlConfiguration defConfig; 191 if (isStrictlyUTF8() || FileConfiguration.UTF8_OVERRIDE) { 192 defConfig = YamlConfiguration.loadConfiguration(new InputStreamReader(defConfigStream, Charsets.UTF_8)); 193 } else { 194 final byte[] contents; 195 defConfig = new YamlConfiguration(); 196 try { 197 contents = ByteStreams.toByteArray(defConfigStream); 198 } catch (final IOException e) { 199 getLogger().log(Level.SEVERE, "Unexpected failure reading config.yml", e); 200 return; 201 } 202 203 final String text = new String(contents, Charset.defaultCharset()); 204 if (!text.equals(new String(contents, Charsets.UTF_8))) { 205 getLogger().warning("Default system encoding may have misread config.yml from plugin jar"); 206 } 207 208 try { 209 defConfig.loadFromString(text); 210 } catch (final InvalidConfigurationException e) { 211 getLogger().log(Level.SEVERE, "Cannot load configuration from jar", e); 212 } 213 } 214 215 newConfig.setDefaults(defConfig); 216 } 217 218 private boolean isStrictlyUTF8() { 219 return getDescription().getAwareness().contains(PluginAwareness.Flags.UTF8); 220 } 221 222 @Override 223 public void saveConfig() { 224 try { 225 getConfig().save(configFile); 226 } catch (IOException ex) { 227 logger.log(Level.SEVERE, "Could not save config to " + configFile, ex); 228 } 229 } 230 231 @Override 232 public void saveDefaultConfig() { 233 if (!configFile.exists()) { 234 saveResource("config.yml", false); 235 } 236 } 237 238 @Override 239 public void saveResource(String resourcePath, boolean replace) { 240 if (resourcePath == null || resourcePath.equals("")) { 241 throw new IllegalArgumentException("ResourcePath cannot be null or empty"); 242 } 243 244 resourcePath = resourcePath.replace('\\', '/'); 245 InputStream in = getResource(resourcePath); 246 if (in == null) { 247 throw new IllegalArgumentException("The embedded resource '" + resourcePath + "' cannot be found in " + file); 248 } 249 250 File outFile = new File(dataFolder, resourcePath); 251 int lastIndex = resourcePath.lastIndexOf('/'); 252 File outDir = new File(dataFolder, resourcePath.substring(0, lastIndex >= 0 ? lastIndex : 0)); 253 254 if (!outDir.exists()) { 255 outDir.mkdirs(); 256 } 257 258 try { 259 if (!outFile.exists() || replace) { 260 OutputStream out = new FileOutputStream(outFile); 261 byte[] buf = new byte[1024]; 262 int len; 263 while ((len = in.read(buf)) > 0) { 264 out.write(buf, 0, len); 265 } 266 out.close(); 267 in.close(); 268 } else { 269 logger.log(Level.WARNING, "Could not save " + outFile.getName() + " to " + outFile + " because " + outFile.getName() + " already exists."); 270 } 271 } catch (IOException ex) { 272 logger.log(Level.SEVERE, "Could not save " + outFile.getName() + " to " + outFile, ex); 273 } 274 } 275 276 @Override 277 public InputStream getResource(String filename) { 278 if (filename == null) { 279 throw new IllegalArgumentException("Filename cannot be null"); 280 } 281 282 try { 283 URL url = getClassLoader().getResource(filename); 284 285 if (url == null) { 286 return null; 287 } 288 289 URLConnection connection = url.openConnection(); 290 connection.setUseCaches(false); 291 return connection.getInputStream(); 292 } catch (IOException ex) { 293 return null; 294 } 295 } 296 297 /** 298 * Returns the ClassLoader which holds this plugin 299 * 300 * @return ClassLoader holding this plugin 301 */ 302 protected final ClassLoader getClassLoader() { 303 return classLoader; 304 } 305 306 /** 307 * Sets the enabled state of this plugin 308 * 309 * @param enabled true if enabled, otherwise false 310 */ 311 protected final void setEnabled(final boolean enabled) { 312 if (isEnabled != enabled) { 313 isEnabled = enabled; 314 315 if (isEnabled) { 316 onEnable(); 317 } else { 318 onDisable(); 319 } 320 } 321 } 322 323 /** 324 * @deprecated This method is legacy and will be removed - it must be 325 * replaced by the specially provided constructor(s). 326 */ 327 @Deprecated 328 protected final void initialize(PluginLoader loader, Server server, PluginDescriptionFile description, File dataFolder, File file, ClassLoader classLoader) { 329 if (server.getWarningState() == WarningState.OFF) { 330 return; 331 } 332 getLogger().log(Level.WARNING, getClass().getName() + " is already initialized", server.getWarningState() == WarningState.DEFAULT ? null : new AuthorNagException("Explicit initialization")); 333 } 334 335 final void init(PluginLoader loader, Server server, PluginDescriptionFile description, File dataFolder, File file, ClassLoader classLoader) { 336 this.loader = loader; 337 this.server = server; 338 this.file = file; 339 this.description = description; 340 this.dataFolder = dataFolder; 341 this.classLoader = classLoader; 342 this.configFile = new File(dataFolder, "config.yml"); 343 this.logger = new PluginLogger(this); 344 345 if (description.isDatabaseEnabled()) { 346 ServerConfig db = new ServerConfig(); 347 348 db.setDefaultServer(false); 349 db.setRegister(false); 350 db.setClasses(getDatabaseClasses()); 351 db.setName(description.getName()); 352 server.configureDbConfig(db); 353 354 DataSourceConfig ds = db.getDataSourceConfig(); 355 356 ds.setUrl(replaceDatabaseString(ds.getUrl())); 357 dataFolder.mkdirs(); 358 359 ClassLoader previous = Thread.currentThread().getContextClassLoader(); 360 361 Thread.currentThread().setContextClassLoader(classLoader); 362 ebean = EbeanServerFactory.create(db); 363 Thread.currentThread().setContextClassLoader(previous); 364 } 365 } 366 367 /** 368 * Provides a list of all classes that should be persisted in the database 369 * 370 * @return List of Classes that are Ebeans 371 */ 372 public List<Class<?>> getDatabaseClasses() { 373 return new ArrayList<Class<?>>(); 374 } 375 376 private String replaceDatabaseString(String input) { 377 input = input.replaceAll("\\{DIR\\}", dataFolder.getPath().replaceAll("\\\\", "/") + "/"); 378 input = input.replaceAll("\\{NAME\\}", description.getName().replaceAll("[^\\w_-]", "")); 379 return input; 380 } 381 382 /** 383 * Gets the initialization status of this plugin 384 * 385 * @return true if this plugin is initialized, otherwise false 386 * @deprecated This method cannot return false, as {@link 387 * JavaPlugin} is now initialized in the constructor. 388 */ 389 @Deprecated 390 public final boolean isInitialized() { 391 return true; 392 } 393 394 /** 395 * {@inheritDoc} 396 */ 397 @Override 398 public boolean onCommand(CommandSender sender, Command command, String label, String[] args) { 399 return false; 400 } 401 402 /** 403 * {@inheritDoc} 404 */ 405 @Override 406 public List<String> onTabComplete(CommandSender sender, Command command, String alias, String[] args) { 407 return null; 408 } 409 410 /** 411 * Gets the command with the given name, specific to this plugin. Commands 412 * need to be registered in the {@link PluginDescriptionFile#getCommands() 413 * PluginDescriptionFile} to exist at runtime. 414 * 415 * @param name name or alias of the command 416 * @return the plugin command if found, otherwise null 417 */ 418 public PluginCommand getCommand(String name) { 419 String alias = name.toLowerCase(); 420 PluginCommand command = getServer().getPluginCommand(alias); 421 422 if (command == null || command.getPlugin() != this) { 423 command = getServer().getPluginCommand(description.getName().toLowerCase() + ":" + alias); 424 } 425 426 if (command != null && command.getPlugin() == this) { 427 return command; 428 } else { 429 return null; 430 } 431 } 432 433 @Override 434 public void onLoad() {} 435 436 @Override 437 public void onDisable() {} 438 439 @Override 440 public void onEnable() {} 441 442 @Override 443 public ChunkGenerator getDefaultWorldGenerator(String worldName, String id) { 444 return null; 445 } 446 447 @Override 448 public final boolean isNaggable() { 449 return naggable; 450 } 451 452 @Override 453 public final void setNaggable(boolean canNag) { 454 this.naggable = canNag; 455 } 456 457 @Override 458 public EbeanServer getDatabase() { 459 return ebean; 460 } 461 462 protected void installDDL() { 463 SpiEbeanServer serv = (SpiEbeanServer) getDatabase(); 464 DdlGenerator gen = serv.getDdlGenerator(); 465 466 gen.runScript(false, gen.generateCreateDdl()); 467 } 468 469 protected void removeDDL() { 470 SpiEbeanServer serv = (SpiEbeanServer) getDatabase(); 471 DdlGenerator gen = serv.getDdlGenerator(); 472 473 gen.runScript(true, gen.generateDropDdl()); 474 } 475 476 @Override 477 public final Logger getLogger() { 478 return logger; 479 } 480 481 @Override 482 public String toString() { 483 return description.getFullName(); 484 } 485 486 /** 487 * This method provides fast access to the plugin that has {@link 488 * #getProvidingPlugin(Class) provided} the given plugin class, which is 489 * usually the plugin that implemented it. 490 * <p> 491 * An exception to this would be if plugin's jar that contained the class 492 * does not extend the class, where the intended plugin would have 493 * resided in a different jar / classloader. 494 * 495 * @param clazz the class desired 496 * @return the plugin that provides and implements said class 497 * @throws IllegalArgumentException if clazz is null 498 * @throws IllegalArgumentException if clazz does not extend {@link 499 * JavaPlugin} 500 * @throws IllegalStateException if clazz was not provided by a plugin, 501 * for example, if called with 502 * <code>JavaPlugin.getPlugin(JavaPlugin.class)</code> 503 * @throws IllegalStateException if called from the static initializer for 504 * given JavaPlugin 505 * @throws ClassCastException if plugin that provided the class does not 506 * extend the class 507 */ 508 public static <T extends JavaPlugin> T getPlugin(Class<T> clazz) { 509 Validate.notNull(clazz, "Null class cannot have a plugin"); 510 if (!JavaPlugin.class.isAssignableFrom(clazz)) { 511 throw new IllegalArgumentException(clazz + " does not extend " + JavaPlugin.class); 512 } 513 final ClassLoader cl = clazz.getClassLoader(); 514 if (!(cl instanceof PluginClassLoader)) { 515 throw new IllegalArgumentException(clazz + " is not initialized by " + PluginClassLoader.class); 516 } 517 JavaPlugin plugin = ((PluginClassLoader) cl).plugin; 518 if (plugin == null) { 519 throw new IllegalStateException("Cannot get plugin for " + clazz + " from a static initializer"); 520 } 521 return clazz.cast(plugin); 522 } 523 524 /** 525 * This method provides fast access to the plugin that has provided the 526 * given class. 527 * 528 * @throws IllegalArgumentException if the class is not provided by a 529 * JavaPlugin 530 * @throws IllegalArgumentException if class is null 531 * @throws IllegalStateException if called from the static initializer for 532 * given JavaPlugin 533 */ 534 public static JavaPlugin getProvidingPlugin(Class<?> clazz) { 535 Validate.notNull(clazz, "Null class cannot have a plugin"); 536 final ClassLoader cl = clazz.getClassLoader(); 537 if (!(cl instanceof PluginClassLoader)) { 538 throw new IllegalArgumentException(clazz + " is not provided by " + PluginClassLoader.class); 539 } 540 JavaPlugin plugin = ((PluginClassLoader) cl).plugin; 541 if (plugin == null) { 542 throw new IllegalStateException("Cannot get plugin for " + clazz + " from a static initializer"); 543 } 544 return plugin; 545 } 546 }