001/*
002 * Syncany, www.syncany.org
003 * Copyright (C) 2011-2016 Philipp C. Heckel <philipp.heckel@gmail.com>
004 *
005 * This program is free software: you can redistribute it and/or modify
006 * it under the terms of the GNU General Public License as published by
007 * the Free Software Foundation, either version 3 of the License, or
008 * (at your option) any later version.
009 *
010 * This program is distributed in the hope that it will be useful,
011 * but WITHOUT ANY WARRANTY; without even the implied warranty of
012 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
013 * GNU General Public License for more details.
014 *
015 * You should have received a copy of the GNU General Public License
016 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
017 */
018package org.syncany.plugins;
019
020import java.lang.reflect.Modifier;
021import java.util.ArrayList;
022import java.util.List;
023import java.util.Map;
024import java.util.TreeMap;
025import java.util.logging.Level;
026import java.util.logging.Logger;
027
028import org.syncany.util.StringUtil;
029
030import com.google.common.collect.ImmutableSet;
031import com.google.common.reflect.ClassPath;
032import com.google.common.reflect.ClassPath.ClassInfo;
033
034/**
035 * This class loads and manages all the {@link Plugin}s loaded in the classpath.
036 * It provides two public methods:
037 *
038 * <ul>
039 *  <li>{@link #list()} returns a list of all loaded plugins (as per classpath)</li>
040 *  <li>{@link #get(String) get()} returns a specific plugin, defined by a name</li>
041 * </ul>
042 *
043 * @see Plugin
044 * @author Philipp C. Heckel (philipp.heckel@gmail.com)
045 */
046public class Plugins {
047        private static final Logger logger = Logger.getLogger(Plugins.class.getSimpleName());
048
049        private static final String PLUGIN_PACKAGE_NAME = Plugin.class.getPackage().getName();
050        private static final String PLUGIN_CLASS_SUFFIX = Plugin.class.getSimpleName();
051
052        private static final Map<String, Plugin> plugins = new TreeMap<String, Plugin>();
053
054        /**
055         * Loads and returns a list of all available
056         * {@link Plugin}s.
057         */
058        public static List<Plugin> list() {
059                loadPlugins();
060                return new ArrayList<Plugin>(plugins.values());
061        }
062
063        /**
064         * Loads and returns a list of all {@link Plugin}s
065         * matching the given subclass.
066         */
067        public static <T extends Plugin> List<T> list(Class<T> pluginClass) {
068                loadPlugins();
069                List<T> matchingPlugins = new ArrayList<T>();
070
071                for (Plugin plugin : plugins.values()) {
072                        if (pluginClass.isInstance(plugin)) {
073                                matchingPlugins.add(pluginClass.cast(plugin));
074                        }
075                }
076
077                return matchingPlugins;
078        }
079
080        /**
081         * Loads the {@link Plugin} by a given identifier.
082         *
083         * <p>Note: Unlike the {@link #list()} method, this method is not expected
084         * to take long, because there is no need to read all JARs in the classpath.
085         *
086         * @param pluginId Identifier of the plugin, as defined by {@link Plugin#getId() the plugin ID}
087         * @return Returns an instance of a plugin, or <code>null</code> if no plugin with the given identifier can be found
088         */
089        public static Plugin get(String pluginId) {
090                if (pluginId == null) {
091                        return null;
092                }
093
094                loadPlugin(pluginId);
095
096                if (plugins.containsKey(pluginId)) {
097                        return plugins.get(pluginId);
098                }
099                else {
100                        return null;
101                }
102        }
103
104        public static <T extends Plugin> T get(String pluginId, Class<T> pluginClass) {
105                Plugin plugin = get(pluginId);
106
107                if (pluginId == null || !pluginClass.isInstance(plugin)) {
108                        return null;
109                }
110                else {
111                        return pluginClass.cast(plugin);
112                }
113        }
114
115        private static void loadPlugin(String pluginId) {
116                if (plugins.containsKey(pluginId)) {
117                        return;
118                }
119
120                loadPlugins();
121
122                if (plugins.containsKey(pluginId)) {
123                        return;
124                }
125                else {
126                        logger.log(Level.WARNING, "Could not load plugin (1): " + pluginId + " (not found or issues with loading)");
127                }
128        }
129
130        public static void refresh() {
131                plugins.clear();
132        }
133
134        /**
135         * Loads all plugins in the classpath.
136         *
137         * <p>First loads all classes in the 'org.syncany.plugins' package.
138         * For all classes ending with the 'Plugin' suffix, it tries to load
139         * them, checks whether they inherit from {@link Plugin} and whether
140         * they can be instantiated.
141         */
142        private static void loadPlugins() {
143                try {
144                        ImmutableSet<ClassInfo> pluginPackageSubclasses = ClassPath
145                                .from(Thread.currentThread().getContextClassLoader())
146                                .getTopLevelClassesRecursive(PLUGIN_PACKAGE_NAME);
147
148                        for (ClassInfo classInfo : pluginPackageSubclasses) {
149                                boolean classNameEndWithPluginSuffix = classInfo.getName().endsWith(PLUGIN_CLASS_SUFFIX);
150
151                                if (classNameEndWithPluginSuffix) {
152                                        Class<?> pluginClass = classInfo.load();
153
154                                        String camelCasePluginId = pluginClass.getSimpleName().replace(Plugin.class.getSimpleName(), "");
155                                        String pluginId = StringUtil.toSnakeCase(camelCasePluginId);
156
157                                        boolean isSubclassOfPlugin = Plugin.class.isAssignableFrom(pluginClass);
158                                        boolean canInstantiate = !Modifier.isAbstract(pluginClass.getModifiers());
159                                        boolean pluginAlreadyLoaded = plugins.containsKey(pluginId);
160
161                                        if (isSubclassOfPlugin && canInstantiate && !pluginAlreadyLoaded) {
162                                                logger.log(Level.INFO, "- " + pluginClass.getName());
163
164                                                try {
165                                                        Plugin plugin = (Plugin) pluginClass.newInstance();
166                                                        plugins.put(plugin.getId(), plugin);
167                                                }
168                                                catch (Exception e) {
169                                                        logger.log(Level.WARNING, "Could not load plugin (2): " + pluginClass.getName(), e);
170                                                }
171                                        }
172                                }
173                        }
174                }
175                catch (Exception e) {
176                        throw new RuntimeException("Unable to load plugins.", e);
177                }
178        }
179}