com.chiorichan.plugin.PluginDescriptionFile.java Source code

Java tutorial

Introduction

Here is the source code for com.chiorichan.plugin.PluginDescriptionFile.java

Source

/**
 * This Source Code Form is subject to the terms of the Mozilla Public
 * License, v. 2.0. If a copy of the MPL was not distributed with this
 * file, You can obtain one at http://mozilla.org/MPL/2.0/.
 * Copyright 2015 Chiori-chan. All Right Reserved.
 * 
 * @author Chiori Greene
 * @email chiorigreene@gmail.com
 */
package com.chiorichan.plugin;

import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.InputStream;
import java.io.Reader;
import java.io.Writer;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import org.yaml.snakeyaml.Yaml;
import org.yaml.snakeyaml.constructor.SafeConstructor;

import com.chiorichan.Loader;
import com.chiorichan.RunLevel;
import com.chiorichan.configuration.file.YamlConfiguration;
import com.chiorichan.lang.InvalidDescriptionException;
import com.chiorichan.maven.MavenLibrary;
import com.google.common.collect.ImmutableList;

/**
 * This type is the runtime-container for the information in the plugin.yaml. All plugins must have a respective
 * plugin.yaml. For plugins written in java using the standard plugin loader, this file must be in the root of the jar
 * file.
 * <p>
 * When Bukkit loads a plugin, it needs to know some basic information about it. It reads this information from a YAML file, 'plugin.yaml'. This file consists of a set of attributes, each defined on a new line and with no indentation.
 * <p>
 * Every (almost* every) method corresponds with a specific entry in the plugin.yaml. These are the <b>required</b> entries for every plugin.yaml:
 * <ul>
 * <li>{@link #getName()} - <code>name</code>
 * <li>{@link #getVersion()} - <code>version</code>
 * <li>{@link #getMain()} - <code>main</code>
 * </ul>
 * <p>
 * Failing to include any of these items will throw an exception and cause the server to ignore your plugin.
 * <p>
 * This is a list of the possible yaml keys, with specific details included in the respective method documentations:
 * <table border=1>
 * <tr>
 * <th>Node</th>
 * <th>Method</th>
 * <th>Summary</th>
 * </tr>
 * <tr>
 * <td><code>name</code></td>
 * <td>{@link #getName()}</td>
 * <td>The unique name of plugin</td>
 * </tr>
 * <tr>
 * <td><code>version</code></td>
 * <td>{@link #getVersion()}</td>
 * <td>A plugin revision identifier</td>
 * </tr>
 * <tr>
 * <td><code>main</code></td>
 * <td>{@link #getMain()}</td>
 * <td>The plugin's initial class file</td>
 * </tr>
 * <tr>
 * <td><code>author</code><br>
 * <code>authors</code></td>
 * <td>{@link #getAuthors()}</td>
 * <td>The plugin contributors</td>
 * </tr>
 * <tr>
 * <td><code>description</code></td>
 * <td>{@link #getDescription()}</td>
 * <td>Human readable plugin summary</td>
 * </tr>
 * <tr>
 * <td><code>website</code></td>
 * <td>{@link #getWebsite()}</td>
 * <td>The URL to the plugin's site</td>
 * </tr>
 * <tr>
 * <td><code>prefix</code></td>
 * <td>{@link #getPrefix()}</td>
 * <td>The token to prefix plugin log entries</td>
 * </tr>
 * <tr>
 * <td><code>load</code></td>
 * <td>{@link #getLoad()}</td>
 * <td>The phase of server-startup this plugin will load during</td>
 * </tr>
 * <tr>
 * <td><code>depend</code></td>
 * <td>{@link #getDepend()}</td>
 * <td>Other required plugins</td>
 * </tr>
 * <tr>
 * <td><code>libraries</code></td>
 * <td>{@link #getLibraries()}</td>
 * <td>Required java libraries</td>
 * </tr>
 * <tr>
 * <td><code>softdepend</code></td>
 * <td>{@link #getSoftDepend()}</td>
 * <td>Other plugins that add functionality</td>
 * </tr>
 * <tr>
 * <td><code>loadbefore</code></td>
 * <td>{@link #getLoadBefore()}</td>
 * <td>The inverse softdepend</td>
 * </tr>
 * </table>
 * <p>
 * A plugin.yaml example:<blockquote>
 * 
 * <pre>
 * name: SuperAwesomePlugin
 * version: 1.0.4
 * description: This plugin does something really awesome to the server
 * author: SomeAuthor
 * authors: [SomeAuthor, God, Jesus]
 * website: http://www.superawesomeplugin.com
 * 
 * main: com.superawesomeplugin.plugin.Main
 * depend: [EmailPlugin]
 * 
 * commands:
 *   doit:
 *     description: Does that super awesome thing
 *     aliases: [doit2, ihateyou]
 *     permission: com.chiorichan.destruction
 *     usage: Type /&lt;doit&gt; to do that super awesome thing.
 * </pre>
 * 
 * </blockquote>
 */
public class PluginDescriptionFile {
    private static final Yaml yaml = new Yaml(new SafeConstructor());
    private String name = null;
    private String main = null;
    private String classLoaderOf = null;
    private List<String> depend = null;
    private List<MavenLibrary> libraries = null;
    private List<String> softDepend = null;
    private List<String> loadBefore = null;
    private String version = null;
    private String description = null;
    private List<String> authors = null;
    private String website = null;
    private String prefix = null;
    private RunLevel order = RunLevel.INITIALIZED;

    public PluginDescriptionFile(final File file) throws InvalidDescriptionException, FileNotFoundException {
        loadMap(asMap(yaml.load(new FileInputStream(file))));
    }

    public PluginDescriptionFile(final InputStream stream) throws InvalidDescriptionException {
        loadMap(asMap(yaml.load(stream)));
    }

    public PluginDescriptionFile(final YamlConfiguration descYaml) throws InvalidDescriptionException {
        loadMap(asMap(descYaml));
    }

    /**
     * Loads a PluginDescriptionFile from the specified reader
     * 
     * @param reader
     *            The reader
     * @throws InvalidDescriptionException
     *             If the PluginDescriptionFile is invalid
     */
    public PluginDescriptionFile(final Reader reader) throws InvalidDescriptionException {
        loadMap(asMap(yaml.load(reader)));
    }

    /**
     * Creates a new PluginDescriptionFile with the given detailed
     * 
     * @param pluginName
     *            Name of this plugin
     * @param pluginVersion
     *            Version of this plugin
     * @param mainClass
     *            Full location of the main class of this plugin
     */
    public PluginDescriptionFile(final String pluginName, final String pluginVersion, final String mainClass) {
        name = pluginName;
        version = pluginVersion;
        main = mainClass;
    }

    /**
     * Gives the name of the plugin. This name is a unique identifier for plugins.
     * <ul>
     * <li>Must consist of all alphanumeric characters, underscores, hyphon, and period (a-z,A-Z,0-9, _.-). Any other character will cause the plugin.yaml to fail loading.
     * <li>Used to determine the name of the plugin's data folder. Data folders are placed in the ./plugins/ directory by default, but this behavior should not be relied on.
     * <li>It is good practice to name your jar the same as this, for example 'MyPlugin.jar'.
     * <li>Case sensitive.
     * <li>The is the token referenced in {@link #getDepend()}, {@link #getSoftDepend()}, and {@link #getLoadBefore()}.
     * </ul>
     * <p>
     * In the plugin.yaml, this entry is named <code>name</code>.
     * <p>
     * Example:<blockquote>
     * 
     * <pre>
     * name: MyPlugin
     * </pre>
     * 
     * </blockquote>
     * 
     * @return the name of the plugin
     */
    public String getName() {
        return name;
    }

    /**
     * Gives the version of the plugin.
     * <ul>
     * <li>Version is an arbitrary string, however the most common format is MajorRelease.MinorRelease.Build (eg: 1.4.1).
     * <li>Typically you will increment this every time you release a new feature or bug fix.
     * <li>Displayed when a user types <code>/version PluginName</code>
     * </ul>
     * <p>
     * In the plugin.yaml, this entry is named <code>version</code>.
     * <p>
     * Example:<blockquote>
     * 
     * <pre>
     * version: 1.4.1
     * </pre>
     * 
     * </blockquote>
     * 
     * @return the version of the plugin
     */
    public String getVersion() {
        return version;
    }

    /**
     * Gives the fully qualified name of the main class for a plugin. The format should follow the {@link ClassLoader#loadClass(String)} syntax to successfully be resolved at runtime. For most plugins, this is the
     * class that extends
     * <ul>
     * <li>This must contain the full namespace including the class file itself.
     * <li>If your namespace is <code>com.chiorichan.plugin</code>, and your class file is called <code>MyPlugin</code> then this must be <code>com.chiorichan.plugin.MyPlugin</code>
     * <li>No plugin can use <code>org.bukkit.</code> as a base package for <b>any class</b>, including the main class.
     * </ul>
     * <p>
     * In the plugin.yaml, this entry is named <code>main</code>.
     * <p>
     * Example: <blockquote>
     * 
     * <pre>
     * main: org.bukkit.plugin.MyPlugin
     * </pre>
     * 
     * </blockquote>
     * 
     * @return the fully qualified main class for the plugin
     */
    public String getMain() {
        return main;
    }

    /**
     * Gives a human-friendly description of the functionality the plugin provides.
     * <ul>
     * <li>The description can have multiple lines.
     * <li>Displayed when a user types <code>/version PluginName</code>
     * </ul>
     * <p>
     * In the plugin.yaml, this entry is named <code>description</code>.
     * <p>
     * Example: <blockquote>
     * 
     * <pre>
     * description: This plugin is so 31337. You can set yourself on fire.
     * </pre>
     * 
     * </blockquote>
     * 
     * @return description of this plugin, or null if not specified
     */
    public String getDescription() {
        return description;
    }

    /**
     * Gives the phase of server startup that the plugin should be loaded.
     * <ul>
     * <li>Possible values are in {@link RunLevel}.
     * <li>Defaults to {@link RunLevel#POSTSERVER}.
     * <li>Certain caveats apply to each phase.
     * <li>When different, {@link #getDepend()}, {@link #getSoftDepend()}, and {@link #getLoadBefore()} become relative in order loaded per-phase. If a plugin loads at <code>STARTUP</code>, but a dependency loads at <code>POSTWORLD</code>,
     * the dependency will not be loaded before the plugin is loaded.
     * </ul>
     * <p>
     * In the plugin.yaml, this entry is named <code>load</code>.
     * <p>
     * Example:<blockquote>
     * 
     * <pre>
     * load: STARTUP
     * </pre>
     * 
     * </blockquote>
     * 
     * @return the phase when the plugin should be loaded
     */
    public RunLevel getLoad() {
        return order;
    }

    /**
     * Gives the list of authors for the plugin.
     * <ul>
     * <li>Gives credit to the developer.
     * <li>Used in some server error messages to provide helpful feedback on who to contact when an error occurs.
     * <li>A bukkit.org forum handle or email address is recommended.
     * <li>Is displayed when a user types <code>/version PluginName</code>
     * <li><code>authors</code> must be in <a href="http://en.wikipedia.org/wiki/YAML#Lists">YAML list format</a>.
     * </ul>
     * <p>
     * In the plugin.yaml, this has two entries, <code>author</code> and <code>authors</code>.
     * <p>
     * Single author example: <blockquote>
     * 
     * <pre>
     * author: CaptainInflamo
     * </pre>
     * 
     * </blockquote> Multiple author example: <blockquote>
     * 
     * <pre>
     * authors: [Cogito, verrier, EvilSeph]
     * </pre>
     * 
     * </blockquote> When both are specified, author will be the first entry in the list, so this example: <blockquote>
     * 
     * <pre>
     * author: Grum
     * authors:
     * - feildmaster
     * - amaranth
     * </pre>
     * 
     * </blockquote> Is equivilant to this example: <blockquote>
     * 
     * <pre>authors: [Grum, feildmaster, aramanth]
     * 
     * <pre>
     * </blockquote>
     * 
     * @return an immutable list of the plugin's authors
     */
    public List<String> getAuthors() {
        return authors;
    }

    /**
     * Gives the plugin's or plugin's author's website.
     * <ul>
     * <li>A link to the Curse page that includes documentation and downloads is highly recommended.
     * <li>Displayed when a user types <code>/version PluginName</code>
     * </ul>
     * <p>
     * In the plugin.yaml, this entry is named <code>website</code>.
     * <p>
     * Example: <blockquote>
     * 
     * <pre>
     * website: http://www.curse.com/server-mods/minecraft/myplugin
     * </pre>
     * 
     * </blockquote>
     * 
     * @return description of this plugin, or null if not specified
     */
    public String getWebsite() {
        return website;
    }

    /**
     * Gives a list of other plugins that the plugin requires.
     * <ul>
     * <li>Use the value in the {@link #getName()} of the target plugin to specify the dependency.
     * <li>If any plugin listed here is not found, your plugin will fail to load at startup.
     * <li>If multiple plugins list each other in <code>depend</code>, creating a network with no individual plugin does not list another plugin in the <a href=https://en.wikipedia.org/wiki/Circular_dependency>network</a>, all plugins in
     * that network will fail.
     * <li><code>depend</code> must be in must be in <a href="http://en.wikipedia.org/wiki/YAML#Lists">YAML list format</a>.
     * </ul>
     * <p>
     * In the plugin.yaml, this entry is named <code>depend</code>.
     * <p>
     * Example: <blockquote>
     * 
     * <pre>
     * depend:
     * - OnePlugin
     * - AnotherPlugin
     * </pre>
     * 
     * </blockquote>
     * 
     * @return immutable list of the plugin's dependencies
     */
    public List<String> getDepend() {
        return depend;
    }

    /**
     * Gives a list of java libraries required by this plugin.
     * <ul>
     * <li>Use the maven group:name:version string to specify the library.
     * <li>If any libraries listed here are not found, your plugin will fail to load at startup.
     * <li><code>libraries</code> must be in must be in <a href="http://en.wikipedia.org/wiki/YAML#Lists">YAML list format</a>.
     * <li>For the time being, libraries are downloaded from the Central Maven Repository. We have plans to implement to ability to specify libraries.
     * </ul>
     * <p>
     * In the plugin.yaml, this entry is named <code>libraries</code>.
     * <p>
     * Example: <blockquote>
     * 
     * <pre>
     * libraries:
     * - com.dropbox.core:dropbox-core-sdk:1.7.7
     * - org.apache.commons:commons-lang3:3.3.2
     * </pre>
     * 
     * </blockquote>
     * 
     * @return immutable list of the plugin's dependencies
     */
    public List<MavenLibrary> getLibraries() {
        return libraries;
    }

    /**
     * Gives a list of other plugins that the plugin requires for full functionality. The {@link PluginManager} will make
     * best effort to treat all entries here as if they were a {@link #getDepend() dependency}, but will never fail
     * because of one of these entries.
     * <ul>
     * <li>Use the value in the {@link #getName()} of the target plugin to specify the dependency.
     * <li>When an unresolvable plugin is listed, it will be ignored and does not affect load order.
     * <li>When a circular dependency occurs (a network of plugins depending or soft-dependending each other), it will arbitrarily choose a plugin that can be resolved when ignoring soft-dependencies.
     * <li><code>softdepend</code> must be in <a href="http://en.wikipedia.org/wiki/YAML#Lists">YAML list format</a>.
     * </ul>
     * <p>
     * In the plugin.yaml, this entry is named <code>softdepend</code>.
     * <p>
     * Example: <blockquote>
     * 
     * <pre>
     * softdepend: [OnePlugin, AnotherPlugin]
     * </pre>
     * 
     * </blockquote>
     * 
     * @return immutable list of the plugin's preferred dependencies
     */
    public List<String> getSoftDepend() {
        return softDepend;
    }

    /**
     * Gets the list of plugins that should consider this plugin a soft-dependency.
     * <ul>
     * <li>Use the value in the {@link #getName()} of the target plugin to specify the dependency.
     * <li>The plugin should load before any other plugins listed here.
     * <li>Specifying another plugin here is strictly equivalent to having the specified plugin's {@link #getSoftDepend()} include {@link #getName() this plugin}.
     * <li><code>loadbefore</code> must be in <a href="http://en.wikipedia.org/wiki/YAML#Lists">YAML list format</a>.
     * </ul>
     * <p>
     * In the plugin.yaml, this entry is named <code>loadbefore</code>.
     * <p>
     * Example: <blockquote>
     * 
     * <pre>
     * loadbefore:
     * - OnePlugin
     * - AnotherPlugin
     * </pre>
     * 
     * </blockquote>
     * 
     * @return immutable list of plugins that should consider this plugin a soft-dependency
     */
    public List<String> getLoadBefore() {
        return loadBefore;
    }

    /**
     * Gives the token to prefix plugin-specific logging messages with.
     * <ul>
     * <li>This includes all messages using Plugin#getLogger()
     * <li>If not specified, the server uses the plugin's {@link #getName() name}.
     * <li>This should clearly indicate what plugin is being logged.
     * </ul>
     * <p>
     * In the plugin.yaml, this entry is named <code>prefix</code>.
     * <p>
     * Example:<blockquote>
     * 
     * <pre>
     * prefix: ex-why-zee
     * </pre>
     * 
     * </blockquote>
     * 
     * @return the prefixed logging token, or null if not specified
     */
    public String getPrefix() {
        return prefix;
    }

    /**
     * Returns the name of a plugin, including the version. This method is provided for convenience; it uses the {@link #getName()} and {@link #getVersion()} entries.
     * 
     * @return a descriptive name of the plugin and respective version
     */
    public String getFullName() {
        return name + " v" + version;
    }

    public String getClassLoaderOf() {
        return classLoaderOf;
    }

    /**
     * Saves this PluginDescriptionFile to the given writer
     * 
     * @param writer
     *            Writer to output this file to
     */
    public void save(Writer writer) {
        yaml.dump(saveMap(), writer);
    }

    private void loadMap(Map<?, ?> map) throws InvalidDescriptionException {
        try {
            name = map.get("name").toString();

            if (!name.matches("^[A-Za-z0-9 _.-]+$")) {
                throw new InvalidDescriptionException("name '" + name + "' contains invalid characters.");
            }
        } catch (NullPointerException ex) {
            throw new InvalidDescriptionException(ex, "name is not defined");
        } catch (ClassCastException ex) {
            throw new InvalidDescriptionException(ex, "name is of wrong type");
        }

        try {
            version = map.get("version").toString();
        } catch (NullPointerException ex) {
            throw new InvalidDescriptionException(ex, "version is not defined");
        } catch (ClassCastException ex) {
            throw new InvalidDescriptionException(ex, "version is of wrong type");
        }

        try {
            main = map.get("main").toString();
            if (main.startsWith("com.chiori") && !main.startsWith("com.chiorichan.plugin"))
                throw new InvalidDescriptionException("main may not be within the com.chiori namespace");
        } catch (NullPointerException ex) {
            throw new InvalidDescriptionException(ex, "main is not defined");
        } catch (ClassCastException ex) {
            throw new InvalidDescriptionException(ex, "main is of wrong type");
        }

        if (map.get("class-loader-of") != null) {
            classLoaderOf = map.get("class-loader-of").toString();
        }

        if (map.get("depend") != null) {
            ImmutableList.Builder<String> dependBuilder = ImmutableList.<String>builder();
            try {
                for (Object dependency : (Iterable<?>) map.get("depend")) {
                    dependBuilder.add(dependency.toString());
                }
            } catch (ClassCastException ex) {
                throw new InvalidDescriptionException(ex, "depend is of wrong type");
            } catch (NullPointerException e) {
                throw new InvalidDescriptionException(e, "invalid dependency format");
            }
            depend = dependBuilder.build();
        }

        if (map.get("libraries") != null) {
            ImmutableList.Builder<MavenLibrary> libraryBuilder = ImmutableList.<MavenLibrary>builder();
            try {
                for (Object library : (Iterable<?>) map.get("libraries")) {
                    try {
                        libraryBuilder.add(new MavenLibrary(library.toString()));
                    } catch (IllegalAccessException e) {
                        Loader.getLogger().severe("Could not parse the library '" + library.toString()
                                + "' for plugin '" + name + "', it will be ignored:");
                        Loader.getLogger().severe(e.getMessage());
                    }
                }
            } catch (ClassCastException ex) {
                throw new InvalidDescriptionException(ex, "library is of wrong type");
            } catch (NullPointerException e) {
                throw new InvalidDescriptionException(e, "invalid library format");
            }
            libraries = libraryBuilder.build();
        }

        if (map.get("softdepend") != null) {
            ImmutableList.Builder<String> softDependBuilder = ImmutableList.<String>builder();
            try {
                for (Object dependency : (Iterable<?>) map.get("softdepend")) {
                    softDependBuilder.add(dependency.toString());
                }
            } catch (ClassCastException ex) {
                throw new InvalidDescriptionException(ex, "softdepend is of wrong type");
            } catch (NullPointerException ex) {
                throw new InvalidDescriptionException(ex, "invalid soft-dependency format");
            }
            softDepend = softDependBuilder.build();
        }

        if (map.get("loadbefore") != null) {
            ImmutableList.Builder<String> loadBeforeBuilder = ImmutableList.<String>builder();
            try {
                for (Object predependency : (Iterable<?>) map.get("loadbefore")) {
                    loadBeforeBuilder.add(predependency.toString());
                }
            } catch (ClassCastException ex) {
                throw new InvalidDescriptionException(ex, "loadbefore is of wrong type");
            } catch (NullPointerException ex) {
                throw new InvalidDescriptionException(ex, "invalid load-before format");
            }
            loadBefore = loadBeforeBuilder.build();
        }

        if (map.get("website") != null) {
            website = map.get("website").toString();
        }

        if (map.get("description") != null) {
            description = map.get("description").toString();
        }

        if (map.get("load") != null) {
            try {
                order = RunLevel.valueOf(((String) map.get("load")).toUpperCase().replaceAll("\\W", ""));
            } catch (ClassCastException ex) {
                throw new InvalidDescriptionException(ex, "load is of wrong type");
            } catch (IllegalArgumentException ex) {
                throw new InvalidDescriptionException(ex, "load is not a valid choice");
            }
        }

        if (map.get("authors") != null) {
            ImmutableList.Builder<String> authorsBuilder = ImmutableList.<String>builder();
            if (map.get("author") != null) {
                authorsBuilder.add(map.get("author").toString());
            }
            try {
                for (Object o : (Iterable<?>) map.get("authors")) {
                    authorsBuilder.add(o.toString());
                }
            } catch (ClassCastException ex) {
                throw new InvalidDescriptionException(ex, "authors are of wrong type");
            } catch (NullPointerException ex) {
                throw new InvalidDescriptionException(ex, "authors are improperly defined");
            }
            authors = authorsBuilder.build();
        } else if (map.get("author") != null) {
            authors = ImmutableList.of(map.get("author").toString());
        } else {
            authors = ImmutableList.<String>of();
        }

        if (map.get("prefix") != null) {
            prefix = map.get("prefix").toString();
        }
    }

    private Map<String, Object> saveMap() {
        Map<String, Object> map = new HashMap<String, Object>();

        map.put("name", name);
        map.put("main", main);
        map.put("version", version);
        map.put("order", order.toString());

        if (depend != null) {
            map.put("depend", depend);
        }
        if (libraries != null) {
            map.put("libraries", libraries);
        }
        if (softDepend != null) {
            map.put("softdepend", softDepend);
        }
        if (website != null) {
            map.put("website", website);
        }
        if (description != null) {
            map.put("description", description);
        }

        if (authors.size() == 1) {
            map.put("author", authors.get(0));
        } else if (authors.size() > 1) {
            map.put("authors", authors);
        }

        if (classLoaderOf != null) {
            map.put("class-loader-of", classLoaderOf);
        }

        if (prefix != null) {
            map.put("prefix", prefix);
        }

        return map;
    }

    private Map<?, ?> asMap(Object object) throws InvalidDescriptionException {
        if (object instanceof Map) {
            return (Map<?, ?>) object;
        }
        throw new InvalidDescriptionException(object + " is not properly structured.");
    }
}