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;
}-*/;
}