001    package de.deepamehta.core.impl;
002    
003    import de.deepamehta.core.osgi.PluginActivator;
004    import de.deepamehta.core.service.PluginInfo;
005    
006    import org.osgi.framework.Bundle;
007    
008    import java.util.ArrayList;
009    import java.util.HashMap;
010    import java.util.List;
011    import java.util.Map;
012    import java.util.logging.Logger;
013    
014    
015    
016    /**
017     * Activates and deactivates plugins and keeps a pool of activated plugins.
018     * The pool of activated plugins is a shared resource. All access to it is synchronized.
019     * <p>
020     * A PluginManager singleton is hold by the {@link EmbeddedService} and is accessed concurrently
021     * by all bundle activation threads (as created e.g. by the File Install bundle).
022     */
023    class PluginManager {
024    
025        // ---------------------------------------------------------------------------------------------- Instance Variables
026    
027        /**
028         * The pool of activated plugins.
029         *
030         * Hashed by plugin bundle's symbolic name, e.g. "de.deepamehta.topicmaps".
031         */
032        private Map<String, PluginImpl> activatedPlugins = new HashMap();
033    
034        private EmbeddedService dms;
035    
036        private Logger logger = Logger.getLogger(getClass().getName());
037    
038        // ---------------------------------------------------------------------------------------------------- Constructors
039    
040        PluginManager(EmbeddedService dms) {
041            this.dms = dms;
042        }
043    
044        // ----------------------------------------------------------------------------------------- Package Private Methods
045    
046        /**
047         * Activates a plugin and fires activation events.
048         * Called once the plugin's requirements are met (see PluginImpl.checkRequirementsForActivation()).
049         * <p>
050         * After activation posts the PLUGIN_ACTIVATED OSGi event. Then checks if all installed plugins are active, and if
051         * so, fires the {@link CoreEvent.ALL_PLUGINS_ACTIVE} core event.
052         * <p>
053         * If the plugin is already activated, performs nothing. This happens e.g. when a dependent plugin is redeployed.
054         * <p>
055         * Note: this method is synchronized. While a plugin is activated no other plugin must be activated. Otherwise
056         * the "type introduction" mechanism might miss some types. Consider this unsynchronized scenario: plugin B
057         * starts running its migrations just in the moment between plugin A's type introduction and event listener
058         * registration. Plugin A might miss some of the types created by plugin B.
059         */
060        synchronized void activatePlugin(PluginImpl plugin) {
061            // Note: we must not activate a plugin twice.
062            if (!_isPluginActivated(plugin.getUri())) {
063                //
064                _activatePlugin(plugin);
065                //
066                plugin.postPluginActivatedEvent();
067                //
068                if (checkAllPluginsActivated()) {
069                    logger.info("########## All Plugins Active ##########");
070                    dms.fireEvent(CoreEvent.ALL_PLUGINS_ACTIVE);
071                }
072            } else {
073                logger.info("Activation of " + plugin + " ABORTED -- already activated");
074                return;
075            }
076        }
077    
078        synchronized void deactivatePlugin(PluginImpl plugin) {
079            plugin.unregisterListeners();
080            removeFromActivatedPlugins(plugin.getUri());
081        }
082    
083        // ---
084    
085        synchronized boolean isPluginActivated(String pluginUri) {
086            return _isPluginActivated(pluginUri);
087        }
088    
089        // ---
090    
091        synchronized PluginImpl getPlugin(String pluginUri) {
092            PluginImpl plugin = activatedPlugins.get(pluginUri);
093            if (plugin == null) {
094                throw new RuntimeException("Plugin \"" + pluginUri + "\" not found");
095            }
096            return plugin;
097        }
098    
099        synchronized List<PluginInfo> getPluginInfo() {
100            List info = new ArrayList();
101            for (PluginImpl plugin : activatedPlugins.values()) {
102                info.add(plugin.getInfo());
103            }
104            return info;
105        }
106    
107    
108    
109        // ------------------------------------------------------------------------------------------------- Private Methods
110    
111        /**
112         * Activates a plugin.
113         *
114         * Activation comprises:
115         *   - install the plugin in the database (includes migrations, post-install event, type introduction)
116         *   - initialize the plugin
117         *   - register the plugin's event listeners
118         *   - register the plugin's OSGi service
119         *   - add the plugin to the pool of activated plugins
120         */
121        private void _activatePlugin(PluginImpl plugin) {
122            try {
123                logger.info("----- Activating " + plugin + " -----");
124                //
125                plugin.installPluginInDB();
126                plugin.initializePlugin();
127                plugin.registerListeners();
128                plugin.registerPluginService();
129                // Note: the event listeners must be registered *after* the plugin is installed in the database and its
130                // postInstall() hook is triggered (see PluginImpl.installPluginInDB()).
131                // Consider the Access Control plugin: it can't set a topic's creator before the "admin" user is created.
132                //
133                // ### TODO: if initialization fails the plugin's listeners are not registered.
134                // Then, when stopping the plugin unregistering its listeners fails.
135                addToActivatedPlugins(plugin);
136                //
137                logger.info("----- Activation of " + plugin + " complete -----");
138            } catch (Throwable e) {
139                // Note: we want catch a NoClassDefFoundError here (so we state Throwable instead of Exception).
140                // This might happen in initializePlugin() when the plugin's init() hook instantiates a class
141                // from a 3rd-party library which can't be loaded.
142                // If not catched File Install would retry to deploy the bundle every 2 seconds (endlessly).
143                throw new RuntimeException("Activation of " + plugin + " failed", e);
144            }
145        }
146    
147        /**
148         * Checks if all plugins are activated.
149         */
150        private boolean checkAllPluginsActivated() {
151            Bundle[] bundles = dms.bundleContext.getBundles();
152            int plugins = 0;
153            int activated = 0;
154            for (Bundle bundle : bundles) {
155                if (isDeepaMehtaPlugin(bundle)) {
156                    plugins++;
157                    if (_isPluginActivated(bundle.getSymbolicName())) {
158                        activated++;
159                    }
160                }
161            }
162            logger.info("### Bundles total: " + bundles.length +
163                ", DeepaMehta plugins: " + plugins + ", Activated: " + activated);
164            return plugins == activated;
165        }
166    
167        /**
168         * Plugin detection: checks if an arbitrary bundle is a DeepaMehta plugin.
169         */
170        private boolean isDeepaMehtaPlugin(Bundle bundle) {
171            try {
172                String activatorClassName = (String) bundle.getHeaders().get("Bundle-Activator");
173                if (activatorClassName != null) {
174                    Class activatorClass = bundle.loadClass(activatorClassName);    // throws ClassNotFoundException
175                    return PluginActivator.class.isAssignableFrom(activatorClass);
176                } else {
177                    // Note: 3rd party bundles may have no activator
178                    return false;
179                }
180            } catch (Exception e) {
181                throw new RuntimeException("Plugin detection failed for bundle " + bundle, e);
182            }
183        }
184    
185        // ---
186    
187        private void addToActivatedPlugins(PluginImpl plugin) {
188            activatedPlugins.put(plugin.getUri(), plugin);
189        }
190    
191        private void removeFromActivatedPlugins(String pluginUri) {
192            activatedPlugins.remove(pluginUri);
193        }
194    
195        private boolean _isPluginActivated(String pluginUri) {
196            return activatedPlugins.get(pluginUri) != null;
197        }
198    }