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 }