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    }