package org.ovirt.engine.ui.webadmin.plugin; import java.util.ArrayList; import java.util.Collection; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.logging.Logger; import org.ovirt.engine.ui.common.auth.CurrentUser; import org.ovirt.engine.ui.webadmin.plugin.api.ApiOptions; import org.ovirt.engine.ui.webadmin.plugin.api.PluginUiFunctions; import com.google.gwt.core.client.JavaScriptObject; import com.google.gwt.core.client.JsArray; import com.google.gwt.core.client.Scheduler; import com.google.gwt.dom.client.Document; import com.google.gwt.dom.client.IFrameElement; import com.google.gwt.dom.client.Style.BorderStyle; import com.google.gwt.dom.client.Style.Position; import com.google.gwt.dom.client.Style.Unit; import com.google.gwt.event.shared.GwtEvent; import com.google.gwt.event.shared.HasHandlers; import com.google.gwt.user.client.Command; import com.google.inject.Inject; import com.google.web.bindery.event.shared.EventBus; /** * The main component of WebAdmin UI plugin infrastructure. * <p> * This class has following responsibilities: * <ul> * <li>create and expose plugin API * <li>define and load plugins * <li>enforce standard plugin lifecycle * </ul> * <p> * Should be bound as GIN eager singleton, created early on during application startup. */ public class PluginManager implements HasHandlers { public interface PluginsReadyCallback { void onPluginsReady(); } public interface PluginInvocationCondition { boolean canInvoke(Plugin plugin); } private static final PluginInvocationCondition INVOKE_ANY_PLUGIN = plugin -> true; private static final Logger logger = Logger.getLogger(PluginManager.class.getName()); // Maps plugin names to corresponding object representations private final Map<String, Plugin> plugins = new HashMap<>(); // Used to track plugins that need to be pre-loaded before loading the main UI private final Map<String, Boolean> pluginsToPreLoad = new HashMap<>(); // Maps plugin names to scheduled event handler functions invoked via Command interface private final Map<String, List<Command>> scheduledFunctionCommands = new HashMap<>(); private final PluginUiFunctions uiFunctions; private final CurrentUser user; private final EventBus eventBus; // PluginsReadyCallback should be invoked only once and as early as possible private boolean pluginsReadyCallbackInvoked = false; private PluginsReadyCallback pluginsReadyCallback; @Inject public PluginManager(PluginUiFunctions uiFunctions, CurrentUser user, EventBus eventBus) { this.uiFunctions = uiFunctions; this.user = user; this.eventBus = eventBus; exposePluginApi(); defineAndLoadPlugins(); } public void setPluginsReadyCallback(PluginsReadyCallback callback) { this.pluginsReadyCallback = callback; } Plugin getPlugin(String pluginName) { return plugins.get(pluginName); } Collection<Plugin> getPlugins() { return plugins.values(); } void addPlugin(Plugin plugin) { plugins.put(plugin.getName(), plugin); } void scheduleFunctionCommand(String pluginName, Command command) { if (!scheduledFunctionCommands.containsKey(pluginName)) { scheduledFunctionCommands.put(pluginName, new ArrayList<Command>()); } scheduledFunctionCommands.get(pluginName).add(command); } void invokeScheduledFunctionCommands(String pluginName) { List<Command> commands = scheduledFunctionCommands.get(pluginName); if (commands != null) { for (Command c : commands) { c.execute(); } } scheduledFunctionCommands.remove(pluginName); } /** * Defines all plugins that were detected when serving WebAdmin host page, and loads them as necessary. */ void defineAndLoadPlugins() { PluginDefinitions definitions = PluginDefinitions.instance(); if (definitions != null) { JsArray<PluginMetaData> metaDataArray = definitions.getMetaDataArray(); for (int i = 0; i < metaDataArray.length(); i++) { PluginMetaData pluginMetaData = metaDataArray.get(i); if (pluginMetaData != null) { defineAndLoadPlugin(pluginMetaData); } } } Scheduler.get().scheduleDeferred(() -> maybeInvokePluginsReadyCallback()); } /** * Defines a plugin from the given meta-data, and loads it as necessary. */ void defineAndLoadPlugin(PluginMetaData pluginMetaData) { String pluginName = pluginMetaData.getName(); String pluginHostPageUrl = pluginMetaData.getHostPageUrl(); if (pluginName == null || pluginName.trim().isEmpty()) { logger.warning("Plugin name cannot be null or empty"); //$NON-NLS-1$ return; } else if (pluginHostPageUrl == null || pluginHostPageUrl.trim().isEmpty()) { logger.warning("Plugin [" + pluginName + "] has null or empty host page URL"); //$NON-NLS-1$ //$NON-NLS-2$ return; } else if (getPlugin(pluginName) != null) { logger.warning("Plugin [" + pluginName + "] is already defined"); //$NON-NLS-1$ //$NON-NLS-2$ return; } // Create an iframe element used to load the plugin host page IFrameElement iframe = Document.get().createIFrameElement(); iframe.setSrc(pluginHostPageUrl); iframe.setFrameBorder(0); iframe.getStyle().setPosition(Position.ABSOLUTE); iframe.getStyle().setWidth(0, Unit.PT); iframe.getStyle().setHeight(0, Unit.PT); iframe.getStyle().setBorderStyle(BorderStyle.NONE); Plugin plugin = new Plugin(pluginMetaData, iframe); addPlugin(plugin); logger.info("Plugin [" + pluginName + "] is defined to be loaded from URL " + pluginHostPageUrl); //$NON-NLS-1$ //$NON-NLS-2$ // Load the plugin host page if (pluginMetaData.isEnabled()) { loadPlugin(plugin); if (plugin.shouldPreLoad()) { pluginsToPreLoad.put(pluginName, false); logger.info("Plugin [" + pluginName + "] will be pre-loaded"); //$NON-NLS-1$ //$NON-NLS-2$ } } } /** * Loads the given plugin by attaching the corresponding iframe element to DOM. */ void loadPlugin(Plugin plugin) { if (plugin.isInState(PluginState.DEFINED)) { logger.info("Loading plugin [" + plugin.getName() + "]"); //$NON-NLS-1$ //$NON-NLS-2$ Document.get().getBody().appendChild(plugin.getIFrameElement()); plugin.markAsLoading(); } } /** * Invokes an event handler function on all plugins which are currently {@linkplain PluginState#IN_USE in use}. * <p> * {@code functionArgs} represents the argument list to use when calling the given function (can be {@code null}). */ public void invokePluginsNow(String functionName, JsArray<?> functionArgs) { invokePluginsNow(functionName, functionArgs, INVOKE_ANY_PLUGIN); } /** * Invokes an event handler function on all plugins which are currently {@linkplain PluginState#IN_USE in use} and * meet the given condition. * <p> * {@code functionArgs} represents the argument list to use when calling the given function (can be {@code null}). */ public void invokePluginsNow(String functionName, JsArray<?> functionArgs, PluginInvocationCondition condition) { for (Plugin plugin : getPlugins()) { if (plugin.isInState(PluginState.IN_USE) && condition.canInvoke(plugin)) { invokePlugin(plugin, functionName, functionArgs); } } } /** * Invokes an event handler function on all plugins which are currently {@linkplain PluginState#IN_USE in use}, and * schedules invocation of the given function on all plugins that might be put in use later on. * <p> * {@code functionArgs} represents the argument list to use when calling the given function (can be {@code null}). */ public void invokePluginsNowOrLater(String functionName, JsArray<?> functionArgs) { invokePluginsNowOrLater(functionName, functionArgs, INVOKE_ANY_PLUGIN); } /** * Invokes an event handler function on all plugins which are currently {@linkplain PluginState#IN_USE in use} and * meet the given condition, and schedules invocation of the given function on all plugins that might be put in use * later on. * <p> * {@code functionArgs} represents the argument list to use when calling the given function (can be {@code null}). */ public void invokePluginsNowOrLater(final String functionName, final JsArray<?> functionArgs, final PluginInvocationCondition condition) { invokePluginsNow(functionName, functionArgs, condition); for (final Plugin plugin : getPlugins()) { if (!plugin.isInState(PluginState.IN_USE)) { scheduleFunctionCommand(plugin.getName(), () -> { if (plugin.isInState(PluginState.IN_USE) && condition.canInvoke(plugin)) { invokePlugin(plugin, functionName, functionArgs); } }); } } } /** * Invokes an event handler function on the given plugin. * <p> * No checks are performed here, make sure to call this method only in a context that fits the general plugin * lifecycle. * <p> * If the function fails due to uncaught exception for the given plugin, that plugin will be automatically * {@linkplain PluginState#FAILED removed from service}. Callers should therefore never call this method if the * given plugin is already out of service. * <p> * Returns {@code true} if the function completed successfully, or {@code false} if an exception escaped the * function call. */ boolean invokePlugin(final Plugin plugin, final String functionName, JsArray<?> functionArgs) { final String pluginName = plugin.getName(); logger.info("Invoking event handler function [" + functionName + "] for plugin [" + pluginName + "]"); //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$ return plugin.getEventHandlerFunction(functionName).invoke(functionArgs, message -> { logger.severe("Exception caught while invoking event handler function [" + functionName //$NON-NLS-1$ + "] for plugin [" + pluginName + "]: " + message); //$NON-NLS-1$ //$NON-NLS-2$ // Remove the given plugin from service Document.get().getBody().removeChild(plugin.getIFrameElement()); plugin.markAsFailed(); logger.warning("Plugin [" + pluginName + "] removed from service due to failure"); //$NON-NLS-1$ //$NON-NLS-2$ }); } /** * Returns {@code true} when the given plugin can perform actions through the API. * <p> * If {@code allowWhileLoading} is {@code false}, the plugin must be either * {@linkplain PluginState#INITIALIZING initializing} (actions performed from UiInit function), * or {@linkplain PluginState#IN_USE in use} (actions performed from other event handler functions). * <p> * If {@code allowWhileLoading} is {@code true}, above constraint is relaxed by allowing the plugin * to be {@linkplain PluginState#LOADING loading} (at this point, infra expects the plugin to call * {@code ready} API function to signal that the plugin is ready for use). */ boolean validatePluginAction(String pluginName, boolean allowWhileLoading) { Plugin plugin = getPlugin(pluginName); if (plugin == null) { return false; } boolean isInitializingOrInUse = plugin.isInState(PluginState.INITIALIZING) || plugin.isInState(PluginState.IN_USE); boolean isLoading = plugin.isInState(PluginState.LOADING); return isInitializingOrInUse || (allowWhileLoading && isLoading); } /** * Registers an event handler object (object containing plugin event handler functions) for the given plugin. */ void registerPluginEventHandlerObject(String pluginName, JavaScriptObject eventHandlerObject) { Plugin plugin = getPlugin(pluginName); if (plugin == null || eventHandlerObject == null) { return; } // Allow plugin event handler object to be set only once if (plugin.getEventHandlerObject() == null) { plugin.setEventHandlerObject(eventHandlerObject); logger.info("Plugin [" + pluginName + "] has registered the event handler object"); //$NON-NLS-1$ //$NON-NLS-2$ } else { logger.warning("Plugin [" + pluginName + "] has already registered the event handler object"); //$NON-NLS-1$ //$NON-NLS-2$ } } /** * Registers a custom API options object for the given plugin. */ void registerPluginApiOptionsObject(String pluginName, ApiOptions apiOptionsObject) { Plugin plugin = getPlugin(pluginName); if (plugin == null || apiOptionsObject == null) { return; } plugin.setApiOptionsObject(apiOptionsObject); logger.info("Plugin [" + pluginName + "] has registered custom API options object"); //$NON-NLS-1$ //$NON-NLS-2$ } /** * Indicates that the given plugin is {@linkplain PluginState#READY ready for use}. */ void pluginReady(String pluginName) { Plugin plugin = getPlugin(pluginName); if (plugin != null && plugin.isInState(PluginState.LOADING)) { if (plugin.getEventHandlerObject() == null) { logger.warning("Plugin [" + pluginName //$NON-NLS-1$ + "] reports in as ready, but has no event handler object assigned"); //$NON-NLS-1$ return; } plugin.markAsReady(); logger.info("Plugin [" + pluginName + "] reports in as ready"); //$NON-NLS-1$ //$NON-NLS-2$ // Initialize the plugin once it's ready initPlugin(plugin); if (plugin.shouldPreLoad()) { pluginsToPreLoad.put(pluginName, true); } maybeInvokePluginsReadyCallback(); } } /** * Checks if all plugins that need pre-loading have been loaded already, and if so, * invokes the {@link #pluginsReadyCallback}. */ void maybeInvokePluginsReadyCallback() { if (pluginsReadyCallback != null && !pluginsReadyCallbackInvoked && !pluginsToPreLoad.containsValue(false)) { pluginsReadyCallback.onPluginsReady(); pluginsReadyCallbackInvoked = true; } } /** * Attempts to {@linkplain PluginState#INITIALIZING initialize} the given plugin by calling UiInit event handler * function on the corresponding event handler object. * <p> * The UiInit function will be called just once during the lifetime of a plugin. More precisely, UiInit function * will be called: * <ul> * <li>after the plugin reports in as {@linkplain PluginState#READY ready} * <li>before any other event handler functions are invoked by the plugin infrastructure * </ul> * <p> * As part of attempting to initialize the given plugin, all event handler functions that have been * {@linkplain #invokePluginsNowOrLater scheduled} for such plugin will be invoked immediately after the UiInit * function completes successfully. */ void initPlugin(Plugin plugin) { String pluginName = plugin.getName(); // Try to invoke UiInit event handler function if (plugin.isInState(PluginState.READY)) { logger.info("Initializing plugin [" + pluginName + "]"); //$NON-NLS-1$ //$NON-NLS-2$ plugin.markAsInitializing(); if (invokePlugin(plugin, "UiInit", null)) { //$NON-NLS-1$ plugin.markAsInUse(); logger.info("Plugin [" + pluginName + "] is initialized and in use now"); //$NON-NLS-1$ //$NON-NLS-2$ } } // Try to invoke all event handler functions scheduled for this plugin if (plugin.isInState(PluginState.IN_USE)) { invokeScheduledFunctionCommands(pluginName); } } @Override public void fireEvent(GwtEvent<?> event) { eventBus.fireEvent(event); } /** * Returns the configuration object associated with the given plugin, or {@code null} if no such object exists. */ JavaScriptObject getConfigObject(String pluginName) { Plugin plugin = getPlugin(pluginName); return plugin != null ? plugin.getMetaData().getConfigObject() : null; } private native void exposePluginApi() /*-{ var ctx = this; var uiFunctions = ctx.@org.ovirt.engine.ui.webadmin.plugin.PluginManager::uiFunctions; var user = ctx.@org.ovirt.engine.ui.webadmin.plugin.PluginManager::user; var validatePluginAction = function(pluginName) { return ctx.@org.ovirt.engine.ui.webadmin.plugin.PluginManager::validatePluginAction(Ljava/lang/String;Z)(pluginName,false); }; var validateSafePluginAction = function(pluginName) { return ctx.@org.ovirt.engine.ui.webadmin.plugin.PluginManager::validatePluginAction(Ljava/lang/String;Z)(pluginName,true); }; var getEntityType = function(entityTypeName) { return @org.ovirt.engine.ui.webadmin.plugin.entity.EntityType::from(Ljava/lang/String;)(entityTypeName); }; var getAlertType = function(alertTypeName) { return @org.ovirt.engine.ui.common.widget.panel.AlertPanel.Type::from(Ljava/lang/String;)(alertTypeName); }; var sanitizeObject = function(object) { return (object != null) ? object : {}; }; // Define pluginApi function used to construct specific Plugin API instances var pluginApi = function(pluginName) { return new pluginApi.fn.init(pluginName); }; // Define pluginApi.fn as an alias to pluginApi prototype pluginApi.fn = pluginApi.prototype = { pluginName: null, // Initialized in constructor function // Constructor function init: function(pluginName) { this.pluginName = pluginName; return this; }, // Registers plugin event handler functions for later invocation register: function(eventHandlerObject) { ctx.@org.ovirt.engine.ui.webadmin.plugin.PluginManager::registerPluginEventHandlerObject(Ljava/lang/String;Lcom/google/gwt/core/client/JavaScriptObject;)(this.pluginName,sanitizeObject(eventHandlerObject)); }, // Registers custom API options object associated with the plugin options: function(apiOptionsObject) { ctx.@org.ovirt.engine.ui.webadmin.plugin.PluginManager::registerPluginApiOptionsObject(Ljava/lang/String;Lorg/ovirt/engine/ui/webadmin/plugin/api/ApiOptions;)(this.pluginName,sanitizeObject(apiOptionsObject)); }, // Indicates that the plugin is ready for use ready: function() { ctx.@org.ovirt.engine.ui.webadmin.plugin.PluginManager::pluginReady(Ljava/lang/String;)(this.pluginName); }, // Returns the configuration object associated with the plugin configObject: function() { return ctx.@org.ovirt.engine.ui.webadmin.plugin.PluginManager::getConfigObject(Ljava/lang/String;)(this.pluginName); }, addMainTab: function(label, historyToken, contentUrl, options) { if (validatePluginAction(this.pluginName)) { uiFunctions.@org.ovirt.engine.ui.webadmin.plugin.api.PluginUiFunctions::addMainTab(Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Lorg/ovirt/engine/ui/webadmin/plugin/api/TabOptions;)(label,historyToken,contentUrl,sanitizeObject(options)); } }, addSubTab: function(entityTypeName, label, historyToken, contentUrl, options) { if (validatePluginAction(this.pluginName)) { uiFunctions.@org.ovirt.engine.ui.webadmin.plugin.api.PluginUiFunctions::addSubTab(Lorg/ovirt/engine/ui/webadmin/plugin/entity/EntityType;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Lorg/ovirt/engine/ui/webadmin/plugin/api/TabOptions;)(getEntityType(entityTypeName),label,historyToken,contentUrl,sanitizeObject(options)); } }, setTabContentUrl: function(historyToken, contentUrl) { if (validatePluginAction(this.pluginName)) { uiFunctions.@org.ovirt.engine.ui.webadmin.plugin.api.PluginUiFunctions::setTabContentUrl(Ljava/lang/String;Ljava/lang/String;)(historyToken,contentUrl); } }, setTabAccessible: function(historyToken, tabAccessible) { if (validatePluginAction(this.pluginName)) { uiFunctions.@org.ovirt.engine.ui.webadmin.plugin.api.PluginUiFunctions::setTabAccessible(Ljava/lang/String;Z)(historyToken,tabAccessible); } }, addMainTabActionButton: function(entityTypeName, label, actionButtonInterface) { if (validatePluginAction(this.pluginName)) { uiFunctions.@org.ovirt.engine.ui.webadmin.plugin.api.PluginUiFunctions::addMainTabActionButton(Lorg/ovirt/engine/ui/webadmin/plugin/entity/EntityType;Ljava/lang/String;Lorg/ovirt/engine/ui/webadmin/plugin/api/ActionButtonInterface;)(getEntityType(entityTypeName),label,sanitizeObject(actionButtonInterface)); } }, addSubTabActionButton: function(mainTabEntityTypeName, subTabEntityTypeName, label, actionButtonInterface) { if (validatePluginAction(this.pluginName)) { uiFunctions.@org.ovirt.engine.ui.webadmin.plugin.api.PluginUiFunctions::addSubTabActionButton(Lorg/ovirt/engine/ui/webadmin/plugin/entity/EntityType;Lorg/ovirt/engine/ui/webadmin/plugin/entity/EntityType;Ljava/lang/String;Lorg/ovirt/engine/ui/webadmin/plugin/api/ActionButtonInterface;)(getEntityType(mainTabEntityTypeName),getEntityType(subTabEntityTypeName),label,sanitizeObject(actionButtonInterface)); } }, showDialog: function(title, dialogToken, contentUrl, width, height, options) { if (validatePluginAction(this.pluginName)) { uiFunctions.@org.ovirt.engine.ui.webadmin.plugin.api.PluginUiFunctions::showDialog(Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Lorg/ovirt/engine/ui/webadmin/plugin/api/DialogOptions;)(title,dialogToken,contentUrl,width,height,sanitizeObject(options)); } }, setDialogContentUrl: function(dialogToken, contentUrl) { if (validatePluginAction(this.pluginName)) { uiFunctions.@org.ovirt.engine.ui.webadmin.plugin.api.PluginUiFunctions::setDialogContentUrl(Ljava/lang/String;Ljava/lang/String;)(dialogToken,contentUrl); } }, closeDialog: function(dialogToken) { if (validatePluginAction(this.pluginName)) { uiFunctions.@org.ovirt.engine.ui.webadmin.plugin.api.PluginUiFunctions::closeDialog(Ljava/lang/String;)(dialogToken); } }, showAlert: function(alertTypeName, message, options) { if (validatePluginAction(this.pluginName)) { uiFunctions.@org.ovirt.engine.ui.webadmin.plugin.api.PluginUiFunctions::showAlert(Lorg/ovirt/engine/ui/common/widget/panel/AlertPanel$Type;Ljava/lang/String;Lorg/ovirt/engine/ui/webadmin/plugin/api/AlertOptions;)(getAlertType(alertTypeName),message,sanitizeObject(options)); } }, revealPlace: function(historyToken) { if (validatePluginAction(this.pluginName)) { uiFunctions.@org.ovirt.engine.ui.webadmin.plugin.api.PluginUiFunctions::revealPlace(Ljava/lang/String;)(historyToken); } }, setSearchString: function(searchString) { if (validatePluginAction(this.pluginName)) { uiFunctions.@org.ovirt.engine.ui.webadmin.plugin.api.PluginUiFunctions::setSearchString(Ljava/lang/String;)(searchString); } }, loginUserName: function() { if (validateSafePluginAction(this.pluginName)) { return user.@org.ovirt.engine.ui.common.auth.CurrentUser::getFullUserName()(); } }, loginUserId: function() { if (validateSafePluginAction(this.pluginName)) { return user.@org.ovirt.engine.ui.common.auth.CurrentUser::getUserId()(); } }, ssoToken: function() { if (validateSafePluginAction(this.pluginName)) { return user.@org.ovirt.engine.ui.common.auth.CurrentUser::getSsoToken()(); } }, engineBaseUrl: function() { if (validateSafePluginAction(this.pluginName)) { return @org.ovirt.engine.ui.frontend.utils.FrontendUrlUtils::getRootURL()() + @org.ovirt.engine.ui.frontend.utils.BaseContextPathData::getRelativePath()(); } }, currentLocale: function() { if (validateSafePluginAction(this.pluginName)) { return uiFunctions.@org.ovirt.engine.ui.webadmin.plugin.api.PluginUiFunctions::getCurrentLocale()(); } }, rootTag: function() { if (validateSafePluginAction(this.pluginName)) { return uiFunctions.@org.ovirt.engine.ui.webadmin.plugin.api.PluginUiFunctions::getRootTagNode()(); } } }; // Give init function the pluginApi prototype for later instantiation pluginApi.fn.init.prototype = pluginApi.fn; // Expose pluginApi function as a global object $wnd.pluginApi = pluginApi; }-*/; }