All Downloads are FREE. Search and download functionalities are using the official Maven repository.

play.plugins.PluginCollection Maven / Gradle / Ivy

There is a newer version: 1.5.0
Show newest version
package play.plugins;

import java.io.BufferedReader;
import java.io.InputStreamReader;
import java.lang.annotation.Annotation;
import java.lang.reflect.Constructor;
import java.lang.reflect.Method;
import java.lang.reflect.Type;
import java.net.URL;
import java.util.AbstractCollection;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.Enumeration;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.ListIterator;
import java.util.Map;
import java.util.Set;
import java.util.SortedSet;
import java.util.TreeSet;

import play.Logger;
import play.Play;
import play.PlayPlugin;
import play.classloading.ApplicationClasses;
import play.classloading.ApplicationClassloader;
import play.data.binding.RootParamNode;
import play.db.Model;
import play.exceptions.UnexpectedException;
import play.libs.F;
import play.mvc.Http;
import play.mvc.Router;
import play.mvc.results.Result;
import play.templates.BaseTemplate;
import play.templates.Template;
import play.test.BaseTest;
import play.test.TestEngine;
import play.vfs.VirtualFile;

/**
 * Class handling all plugins used by Play.
 *
 * Loading/reloading/enabling/disabling is handled here.
 *
 * This class also exposes many PlayPlugin-methods which when called, the method
 * is executed on all enabled plugins.
 *
 * Since all the enabled-plugins-iteration is done here, the code elsewhere is
 * cleaner.
 */
public class PluginCollection {

    /**
     * Property holding the name of the play.plugins-resource-name. Can be
     * modified in unittest to supply modifies plugin-list
     */
    protected String play_plugins_resourceName = "play.plugins";

    /**
     * List that holds all loaded plugins, enabled or disabled
     */
    protected List allPlugins = new ArrayList<>();

    /**
     * Readonly copy of allPlugins - updated each time allPlugins is updated.
     * Using this cached copy so we don't have to create it all the time..
     */
    protected List allPlugins_readOnlyCopy = createReadonlyCopy(allPlugins);

    /**
     * List of all enabled plugins
     */
    protected List enabledPlugins = new ArrayList<>();

    /**
     * Readonly copy of enabledPlugins - updated each time enabledPlugins is
     * updated. Using this cached copy so we don't have to create it all the
     * time
     */
    protected List enabledPlugins_readOnlyCopy = createReadonlyCopy(enabledPlugins);

    /**
     * List of all enabled plugins with filters
     */
    protected List enabledPluginsWithFilters = new ArrayList<>();

    /**
     * Readonly copy of enabledPluginsWithFilters - updated each time
     * enabledPluginsWithFilters is updated. Using this cached copy so we don't
     * have to create it all the time
     */
    protected List enabledPluginsWithFilters_readOnlyCopy = createReadonlyCopy(enabledPluginsWithFilters);

    /**
     * Using readonly list to crash if someone tries to modify the copy.
     * 
     * @param list
     * @return Read only list of plugins
     */
    protected List createReadonlyCopy(List list) {
        return Collections.unmodifiableList(new ArrayList<>(list));
    }

    private static class LoadingPluginInfo implements Comparable {
        public final String name;
        public final int index;
        public final URL url;

        private LoadingPluginInfo(String name, int index, URL url) {
            this.name = name;
            this.index = index;
            this.url = url;
        }

        @Override
        public String toString() {
            return "LoadingPluginInfo{" + "name='" + name + '\'' + ", index=" + index + ", url=" + url + '}';
        }

        @Override
        public int compareTo(LoadingPluginInfo o) {
            int res = index < o.index ? -1 : (index == o.index ? 0 : 1);
            if (res != 0) {
                return res;
            }

            // Index is equal in both plugins.
            // sort on name to get consistent order
            return name.compareTo(o.name);
        }

        @Override
        public boolean equals(Object o) {
            if (this == o)
                return true;
            if (o == null || getClass() != o.getClass())
                return false;

            LoadingPluginInfo that = (LoadingPluginInfo) o;

            if (index != that.index)
                return false;
            if (name != null ? !name.equals(that.name) : that.name != null)
                return false;

            return true;
        }

        @Override
        public int hashCode() {
            int result = name != null ? name.hashCode() : 0;
            result = 31 * result + index;
            return result;
        }
    }

    /**
     * Enable found plugins
     */
    public void loadPlugins() {
        Logger.trace("Loading plugins");
        // Play! plugins
        Enumeration urls = null;
        try {
            urls = Play.classloader.getResources(play_plugins_resourceName);
        } catch (Exception e) {
            Logger.error("Error loading play.plugins", e);
            return;
        }

        // First we build one big SortedSet of all plugins to load (sorted based
        // on index)
        // This must be done to make sure the enhancing is happening
        // when loading plugins using other classes that must be enhanced.
        // Data structure is a SortedSet instead of a List to avoid including
        // the same class+index twice --
        // this happened in the past under a range of circumstances, including:
        // 1. Class path on NTFS or other case insensitive file system includes
        // play.plugins directory 2x
        // (C:/myproject/conf;c:/myproject/conf)
        // 2.
        // https://play.lighthouseapp.com/projects/57987/tickets/176-app-playplugins-loaded-twice-conf-on-2-classpaths
        // I can see loading the same plugin with different indexes, but I can't
        // think of a reasonable use case for
        // loading the same plugin multiple times at the same priority.
        SortedSet pluginsToLoad = new TreeSet<>();
        while (urls != null && urls.hasMoreElements()) {
            URL url = urls.nextElement();
            Logger.trace("Found one plugins descriptor, %s", url);
            try {
                BufferedReader reader = new BufferedReader(new InputStreamReader(url.openStream(), "utf-8"));
                String line;
                while ((line = reader.readLine()) != null) {
                    if (line.trim().length() == 0) {
                        continue;
                    }
                    String[] lineParts = line.split(":");
                    LoadingPluginInfo info = new LoadingPluginInfo(lineParts[1].trim(), Integer.parseInt(lineParts[0]), url);
                    pluginsToLoad.add(info);
                }
            } catch (Exception e) {
                Logger.error(e, "Error interpreting %s", url);
            }

        }

        for (LoadingPluginInfo info : pluginsToLoad) {
            Logger.trace("Loading plugin %s", info.name);
            try {
                PlayPlugin plugin = (PlayPlugin) Play.classloader.loadClass(info.name).newInstance();
                plugin.index = info.index;
                if (addPlugin(plugin)) {
                    Logger.trace("Plugin %s loaded", plugin);
                } else {
                    Logger.warn("Did not load plugin %s. Already loaded", plugin);
                }
            } catch (Exception ex) {
                Logger.error(ex, "Error loading plugin %s", info.toString());
            }
        }
        // Mow we must call onLoad for all plugins - and we must detect if a
        // plugin
        // disables another plugin the old way, by removing it from
        // Play.plugins.
        for (PlayPlugin plugin : getEnabledPlugins()) {

            // is this plugin still enabled?
            if (isEnabled(plugin)) {
                initializePlugin(plugin);
            }
        }

        // Must update Play.plugins-list one last time
        updatePlayPluginsList();

    }

    /**
     * Reloads all loaded plugins that is application-supplied.
     */
    public void reloadApplicationPlugins() throws Exception {

        Set reloadedPlugins = new HashSet<>();
        for (PlayPlugin plugin : getAllPlugins()) {

            // Is this plugin an application-supplied-plugin?
            if (isLoadedByApplicationClassloader(plugin)) {
                // This plugin is application-supplied - Must reload it
                String pluginClassName = plugin.getClass().getName();
                Class pluginClazz = Play.classloader.loadClass(pluginClassName);

                // First look for constructors the old way
                Constructor[] constructors = pluginClazz.getConstructors();

                if (constructors.length == 0) {
                    // No constructors in plugin
                    // using getDeclaredConstructors() instead of
                    // getConstructors() to make it work for plugins without
                    // constructor
                    constructors = pluginClazz.getDeclaredConstructors();
                }

                PlayPlugin newPlugin = (PlayPlugin) constructors[0].newInstance();
                newPlugin.index = plugin.index;
                // Replace this plugin
                replacePlugin(plugin, newPlugin);
                reloadedPlugins.add(newPlugin);
            }
        }

        // Now we must call onLoad for all reloaded plugins
        for (PlayPlugin plugin : reloadedPlugins) {
            initializePlugin(plugin);
        }

        updatePlayPluginsList();

    }

    protected boolean isLoadedByApplicationClassloader(PlayPlugin plugin) {
        return plugin.getClass().getClassLoader().getClass().equals(ApplicationClassloader.class);
    }

    /**
     * Calls plugin.onLoad but detects if plugin removes other plugins from
     * Play.plugins-list to detect if plugins disables a plugin the old hacked
     * way..
     * 
     * @param plugin
     */
    @SuppressWarnings({ "deprecation" })
    protected void initializePlugin(PlayPlugin plugin) {
        Logger.trace("Initializing plugin " + plugin);
        // We're ready to call onLoad for this plugin.
        // must create a unique Play.plugins-list for this onLoad-method-call so
        // we can detect if some plugins are removed/disabled
        Play.plugins = new ArrayList<>(getEnabledPlugins());
        plugin.onLoad();
        // Check for missing/removed plugins
        for (PlayPlugin enabledPlugin : getEnabledPlugins()) {
            if (!Play.plugins.contains(enabledPlugin)) {
                Logger.info("Detected that plugin '" + plugin + "' disabled the plugin '" + enabledPlugin
                        + "' the old way - should use Play.disablePlugin()");
                // This enabled plugin was disabled.
                // must disable it in pluginCollection
                disablePlugin(enabledPlugin);
            }
        }
    }

    /**
     * Adds one plugin and enables it
     * 
     * @param plugin
     * @return true if plugin was new and was added
     */
    protected synchronized boolean addPlugin(PlayPlugin plugin) {
        if (!allPlugins.contains(plugin)) {
            allPlugins.add(plugin);
            Collections.sort(allPlugins);
            allPlugins_readOnlyCopy = createReadonlyCopy(allPlugins);
            enablePlugin(plugin);
            return true;
        }
        return false;
    }

    protected synchronized void replacePlugin(PlayPlugin oldPlugin, PlayPlugin newPlugin) {
        if (allPlugins.remove(oldPlugin)) {
            allPlugins.add(newPlugin);
            Collections.sort(allPlugins);
            allPlugins_readOnlyCopy = createReadonlyCopy(allPlugins);
        }

        if (enabledPlugins.remove(oldPlugin)) {
            enabledPlugins.add(newPlugin);
            Collections.sort(enabledPlugins);
            enabledPlugins_readOnlyCopy = createReadonlyCopy(enabledPlugins);

            if (enabledPluginsWithFilters.remove(oldPlugin) && newPlugin.hasFilter()) {
                enabledPluginsWithFilters.add(newPlugin);
                Collections.sort(enabledPluginsWithFilters);
                enabledPluginsWithFilters_readOnlyCopy = createReadonlyCopy(enabledPluginsWithFilters);
            }
        }

    }

    /**
     * Enable plugin.
     *
     * @param plugin
     * @return true if plugin exists and was enabled now
     */
    public synchronized boolean enablePlugin(PlayPlugin plugin) {
        if (allPlugins.contains(plugin)) {
            // the plugin exists
            if (!enabledPlugins.contains(plugin)) {
                // plugin not currently enabled
                enabledPlugins.add(plugin);
                Collections.sort(enabledPlugins);
                enabledPlugins_readOnlyCopy = createReadonlyCopy(enabledPlugins);

                if (plugin.hasFilter()) {
                    enabledPluginsWithFilters.add(plugin);
                    Collections.sort(enabledPluginsWithFilters);
                    enabledPluginsWithFilters_readOnlyCopy = createReadonlyCopy(enabledPluginsWithFilters);
                }

                updatePlayPluginsList();
                Logger.trace("Plugin " + plugin + " enabled");
                return true;
            }
        }

        return false;
    }

    /**
     * enable plugin of specified type
     * 
     * @return true if plugin was enabled
     */
    public boolean enablePlugin(Class pluginClazz) {
        return enablePlugin(getPluginInstance(pluginClazz));
    }

    /**
     * Returns the first instance of a loaded plugin of specified type
     * 
     * @param pluginClazz
     * @return PlayPlugin
     */
    public synchronized PlayPlugin getPluginInstance(Class pluginClazz) {
        for (PlayPlugin p : getAllPlugins()) {
            if (pluginClazz.isInstance(p)) {
                return p;
            }
        }
        return null;
    }

    /**
     * disable plugin
     * 
     * @param plugin
     * @return true if plugin was enabled and now is disabled
     */
    public synchronized boolean disablePlugin(PlayPlugin plugin) {
        // try to disable it?
        if (enabledPlugins.remove(plugin)) {
            // plugin was removed
            enabledPlugins_readOnlyCopy = createReadonlyCopy(enabledPlugins);

            if (enabledPluginsWithFilters.remove(plugin)) {
                enabledPluginsWithFilters_readOnlyCopy = createReadonlyCopy(enabledPluginsWithFilters);
            }

            updatePlayPluginsList();
            Logger.trace("Plugin " + plugin + " disabled");
            return true;
        }
        return false;
    }

    /**
     * disable plugin of specified type
     * 
     * @return true if plugin was enabled and now is disabled
     */
    public boolean disablePlugin(Class pluginClazz) {
        return disablePlugin(getPluginInstance(pluginClazz));
    }

    /**
     * Must update Play.plugins-list to be backward compatible
     */
    @SuppressWarnings({ "deprecation" })
    public void updatePlayPluginsList() {
        Play.plugins = Collections.unmodifiableList(getEnabledPlugins());
    }

    /**
     * Returns new readonly list of all enabled plugins
     * 
     * @return List of plugins
     */
    public List getEnabledPlugins() {
        return enabledPlugins_readOnlyCopy;
    }

    /**
     * Returns new readonly list of all enabled plugins that define filters.
     * 
     * @return List of plugins
     */
    public List getEnabledPluginsWithFilters() {
        return enabledPluginsWithFilters_readOnlyCopy;
    }

    @SuppressWarnings("unchecked")
    public  F.Option> composeFilters() {
        // Copy list of plugins here in case the list changes in the midst of
        // doing composition...
        // (Is it really necessary to do this?)
        List pluginsWithFilters = new ArrayList<>(this.getEnabledPluginsWithFilters());

        if (pluginsWithFilters.isEmpty()) {
            return F.Option.None();
        } else {
            Iterator itr = pluginsWithFilters.iterator();
            PlayPlugin.Filter ret = itr.next().getFilter();
            while (itr.hasNext()) {
                ret = ret. decorate(itr.next().getFilter());
            }
            return F.Option.Some(ret);
        }
    }

    /**
     * Returns readonly view of all enabled plugins in reversed order
     * 
     * @return Collection of plugins
     */
    public Collection getReversedEnabledPlugins() {
        return new AbstractCollection() {

            @Override
            public Iterator iterator() {
                final ListIterator enabledPluginsListIt = enabledPlugins.listIterator(size() - 1);
                return new Iterator() {

                    @Override
                    public boolean hasNext() {
                        return enabledPluginsListIt.hasPrevious();
                    }

                    @Override
                    public PlayPlugin next() {
                        return enabledPluginsListIt.previous();
                    }

                    @Override
                    public void remove() {
                        enabledPluginsListIt.remove();
                    }
                };
            }

            @Override
            public int size() {
                return enabledPlugins.size();
            }

        };
    }

    /**
     * Returns new readonly list of all plugins
     * 
     * @return List of plugins
     */
    public List getAllPlugins() {
        return allPlugins_readOnlyCopy;
    }

    /**
     *
     * @param plugin
     * @return true if plugin is enabled
     */
    public boolean isEnabled(PlayPlugin plugin) {
        return getEnabledPlugins().contains(plugin);
    }

    public boolean compileSources() {
        for (PlayPlugin plugin : getEnabledPlugins()) {
            if (plugin.compileSources()) {
                return true;
            }
        }
        return false;
    }

    public boolean detectClassesChange() {
        for (PlayPlugin plugin : getEnabledPlugins()) {
            if (plugin.detectClassesChange()) {
                return true;
            }
        }
        return false;
    }

    public void invocationFinally() {
        for (PlayPlugin plugin : getEnabledPlugins()) {
            plugin.invocationFinally();
        }
    }

    public void beforeInvocation() {
        for (PlayPlugin plugin : getEnabledPlugins()) {
            plugin.beforeInvocation();
        }
    }

    public void afterInvocation() {
        for (PlayPlugin plugin : getEnabledPlugins()) {
            plugin.afterInvocation();
        }
    }

    public void onInvocationSuccess() {
        for (PlayPlugin plugin : getEnabledPlugins()) {
            plugin.onInvocationSuccess();
        }
    }

    public void onInvocationException(Throwable e) {
        for (PlayPlugin plugin : getEnabledPlugins()) {
            try {
                plugin.onInvocationException(e);
            } catch (Throwable ex) {
                Logger.error(ex, "Failed to handle invocation exception by plugin %s", plugin.getClass().getName());
            }
        }
    }

    public void beforeDetectingChanges() {
        for (PlayPlugin plugin : getEnabledPlugins()) {
            plugin.beforeDetectingChanges();
        }
    }

    public void detectChange() {
        for (PlayPlugin plugin : getEnabledPlugins()) {
            plugin.detectChange();
        }
    }

    public void onApplicationReady() {
        for (PlayPlugin plugin : getEnabledPlugins()) {
            plugin.onApplicationReady();
        }
    }

    public void onConfigurationRead() {
        for (PlayPlugin plugin : getEnabledPlugins()) {
            plugin.onConfigurationRead();
        }
    }

    public void onApplicationStart() {
        for (PlayPlugin plugin : getEnabledPlugins()) {
            plugin.onApplicationStart();
        }
    }

    public void afterApplicationStart() {
        for (PlayPlugin plugin : getEnabledPlugins()) {
            plugin.afterApplicationStart();
        }
    }

    public void onApplicationStop() {
        for (PlayPlugin plugin : getReversedEnabledPlugins()) {
            try {
                plugin.onApplicationStop();
            } catch (Throwable t) {
                if (t.getMessage() == null)
                    Logger.error(t, "Error while stopping %s", plugin);
                else if (Logger.isDebugEnabled())
                    Logger.debug(t, "Error while stopping %s", plugin);
                else
                    Logger.info("Error while stopping %s: %s", plugin, t.toString());
            }
        }
    }

    public void onEvent(String message, Object context) {
        for (PlayPlugin plugin : getEnabledPlugins()) {
            plugin.onEvent(message, context);
        }
    }

    public void enhance(ApplicationClasses.ApplicationClass applicationClass) {
        for (PlayPlugin plugin : getEnabledPlugins()) {
            try {
                long start = System.currentTimeMillis();
                plugin.enhance(applicationClass);
                if (Logger.isTraceEnabled()) {
                    Logger.trace("%sms to apply %s to %s", System.currentTimeMillis() - start, plugin, applicationClass.name);
                }
            } catch (Exception e) {
                throw new UnexpectedException("While applying " + plugin + " on " + applicationClass.name, e);
            }
        }
    }

    @Deprecated
    public List onClassesChange(List modified) {
        List modifiedWithDependencies = new ArrayList<>();
        for (PlayPlugin plugin : getEnabledPlugins()) {
            modifiedWithDependencies.addAll(plugin.onClassesChange(modified));
        }
        return modifiedWithDependencies;
    }

    @Deprecated
    public void compileAll(List classes) {
        for (PlayPlugin plugin : getEnabledPlugins()) {
            plugin.compileAll(classes);
        }
    }

    public Object bind(RootParamNode rootParamNode, String name, Class clazz, Type type, Annotation[] annotations) {
        for (PlayPlugin plugin : getEnabledPlugins()) {
            Object result = plugin.bind(rootParamNode, name, clazz, type, annotations);
            if (result != null) {
                return result;
            }
        }
        return null;
    }

    public Object bindBean(RootParamNode rootParamNode, String name, Object bean) {
        for (PlayPlugin plugin : getEnabledPlugins()) {
            Object result = plugin.bindBean(rootParamNode, name, bean);
            if (result != null) {
                return result;
            }
        }
        return null;
    }

    public Map unBind(Object src, String name) {
        for (PlayPlugin plugin : getEnabledPlugins()) {
            Map r = plugin.unBind(src, name);
            if (r != null) {
                return r;
            }
        }
        return null;
    }

    public Object willBeValidated(Object value) {
        for (PlayPlugin plugin : getEnabledPlugins()) {
            Object newValue = plugin.willBeValidated(value);
            if (newValue != null) {
                return newValue;
            }
        }
        return value;
    }

    public Model.Factory modelFactory(Class modelClass) {
        for (PlayPlugin plugin : getEnabledPlugins()) {
            Model.Factory factory = plugin.modelFactory(modelClass);
            if (factory != null) {
                return factory;
            }
        }
        return null;
    }

    public String getMessage(String locale, Object key, Object... args) {
        for (PlayPlugin plugin : getEnabledPlugins()) {
            String message = plugin.getMessage(locale, key, args);
            if (message != null) {
                return message;
            }
        }
        return null;
    }

    public void beforeActionInvocation(Method actionMethod) {
        for (PlayPlugin plugin : getEnabledPlugins()) {
            plugin.beforeActionInvocation(actionMethod);
        }
    }

    public void onActionInvocationResult(Result result) {
        for (PlayPlugin plugin : getEnabledPlugins()) {
            plugin.onActionInvocationResult(result);
        }
    }

    public void afterActionInvocation() {
        for (PlayPlugin plugin : getEnabledPlugins()) {
            plugin.afterActionInvocation();
        }
    }

    public void onActionInvocationFinally() {
        for (PlayPlugin plugin : getEnabledPlugins()) {
            plugin.onActionInvocationFinally();
        }
    }

    public void routeRequest(Http.Request request) {
        for (PlayPlugin plugin : getEnabledPlugins()) {
            plugin.routeRequest(request);
        }
    }

    public void onRequestRouting(Router.Route route) {
        for (PlayPlugin plugin : getEnabledPlugins()) {
            plugin.onRequestRouting(route);
        }
    }

    public void onRoutesLoaded() {
        for (PlayPlugin plugin : getEnabledPlugins()) {
            plugin.onRoutesLoaded();
        }
    }

    public boolean rawInvocation(Http.Request request, Http.Response response) throws Exception {
        for (PlayPlugin plugin : getEnabledPlugins()) {
            if (plugin.rawInvocation(request, response)) {
                return true;
            }
        }
        return false;
    }

    public boolean serveStatic(VirtualFile file, Http.Request request, Http.Response response) {
        for (PlayPlugin plugin : getEnabledPlugins()) {
            if (plugin.serveStatic(file, request, response)) {
                return true;
            }
        }
        return false;
    }

    public List addTemplateExtensions() {
        List list = new ArrayList<>();
        for (PlayPlugin plugin : getEnabledPlugins()) {
            list.addAll(plugin.addTemplateExtensions());
        }
        return list;
    }

    public String overrideTemplateSource(BaseTemplate template, String source) {
        for (PlayPlugin plugin : getEnabledPlugins()) {
            String newSource = plugin.overrideTemplateSource(template, source);
            if (newSource != null) {
                source = newSource;
            }
        }
        return source;
    }

    public Template loadTemplate(VirtualFile file) {
        for (PlayPlugin plugin : getEnabledPlugins()) {
            Template pluginProvided = plugin.loadTemplate(file);
            if (pluginProvided != null) {
                return pluginProvided;
            }
        }
        return null;
    }

    public void afterFixtureLoad() {
        for (PlayPlugin plugin : getEnabledPlugins()) {
            plugin.afterFixtureLoad();
        }
    }

    public TestEngine.TestResults runTest(Class clazz) {
        for (PlayPlugin plugin : getEnabledPlugins()) {
            TestEngine.TestResults pluginTestResults = plugin.runTest(clazz);
            if (pluginTestResults != null) {
                return pluginTestResults;
            }
        }
        return null;
    }

    public Collection getUnitTests() {
        Set allPluginTests = new HashSet<>();
        for (PlayPlugin plugin : getEnabledPlugins()) {
            Collection unitTests = plugin.getUnitTests();
            if (unitTests != null) {
                allPluginTests.addAll(unitTests);
            }
        }

        return allPluginTests;
    }

    public Collection getFunctionalTests() {
        Set allPluginTests = new HashSet<>();
        for (PlayPlugin plugin : getEnabledPlugins()) {
            Collection funcTests = plugin.getFunctionalTests();
            if (funcTests != null) {
                allPluginTests.addAll(funcTests);
            }
        }

        return allPluginTests;
    }
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy