gt:             setTabContentUrl: function(historyToken, contentUrl) {
context: 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 org.ovirt.engine.ui.webadmin.plugin.jsni.JsFunction.ErrorHandler;
  
  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.core.client.Scheduler.ScheduledCommand;
  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.user.client.Command;
  import com.google.inject.Inject;
  
  /**
   * 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 {
  
      public interface PluginInvocationCondition {
  
          boolean canInvoke(Plugin plugin);
  
      }
  
      private static final PluginInvocationCondition INVOKE_ANY_PLUGIN = new PluginInvocationCondition() {
          @Override
          public boolean canInvoke(Plugin plugin) {
              return 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<String, Plugin>();
  
      // Maps plugin names to scheduled event handler functions invoked via Command interface
      private final Map<String, List<Command>> scheduledFunctionCommands = new HashMap<String, List<Command>>();
  
      // Controls plugin invocation, allowing WebAdmin to call plugins only in a specific context
      private boolean canInvokePlugins = false;
  
      private final PluginUiFunctions uiFunctions;
      private final CurrentUser user;
  
      @Inject
      public PluginManager(PluginUiFunctions uiFunctions, CurrentUser user) {
          this.uiFunctions = uiFunctions;
          this.user = user;
          exposePluginApi();
          defineAndLoadPlugins();
      }
  
      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);
      }
  
      void cancelScheduledFunctionCommands() {
          scheduledFunctionCommands.clear();
      }
  
      /**
       * 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);
                  }
              }
          }
      }
  
      /**
       * 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$
  
          if (pluginMetaData.isEnabled()) {
              loadPlugin(plugin);
          }
      }
  
      /**
       * 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();
          }
      }
  
      /**
       * Called when WebAdmin enters the state that allows plugins to be invoked.
       */
      public void enablePluginInvocation() {
          canInvokePlugins = true;
  
          // Try to initialize all plugins after the browser event loop returns
          Scheduler.get().scheduleDeferred(new ScheduledCommand() {
              @Override
              public void execute() {
                  for (Plugin plugin : getPlugins()) {
                      initPlugin(plugin);
                  }
              }
          });
      }
  
      /**
       * Called when WebAdmin leaves the state that allows plugins to be invoked.
       */
      public void disablePluginInvocation() {
          canInvokePlugins = false;
  
          // Clean up scheduled event handler functions for all plugins,
          // since we are leaving the current plugin invocation context
          cancelScheduledFunctionCommands();
      }
  
      /**
       * 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) {
          if (canInvokePlugins) {
              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 (!canInvokePlugins || !plugin.isInState(PluginState.IN_USE)) {
                  scheduleFunctionCommand(plugin.getName(), new Command() {
                      @Override
                      public void execute() {
                          if (canInvokePlugins && 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, new ErrorHandler() {
              @Override
              public void onError(String 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>
       * More precisely, returns {@code true} when all of the following conditions are met:
       * <ul>
       * <li>WebAdmin is currently in state that allows plugins to be invoked
       * <li>the plugin is either {@linkplain PluginState#INITIALIZING initializing} (actions performed from UiInit
       * function), or {@linkplain PluginState#IN_USE in use} (actions performed from other event handler functions)
       * </ul>
       */
      boolean canDoPluginAction(String pluginName) {
          Plugin plugin = getPlugin(pluginName);
          boolean pluginInitializingOrInUse = plugin != null
                  ? plugin.isInState(PluginState.INITIALIZING) || plugin.isInState(PluginState.IN_USE) : false;
          return canInvokePlugins && pluginInitializingOrInUse;
      }
  
      /**
       * 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$
  
              // Try to initialize the plugin, since the plugin might report in as ready
              // after WebAdmin enters the state that allows plugins to be invoked
              initPlugin(plugin);
          }
      }
  
      /**
       * 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} <b>and</b> WebAdmin
       * {@linkplain #enablePluginInvocation enters} the state that allows plugins to be invoked
       * <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) {
          if (!canInvokePlugins) {
              return;
          }
  
          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);
          }
      }
  
      /**
       * 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 canDoPluginAction = function(pluginName) {
              return ctx.@org.ovirt.engine.ui.webadmin.plugin.PluginManager::canDoPluginAction(Ljava/lang/String;)(pluginName);
          };
  
          var getEntityType = function(entityTypeName) {
              return @org.ovirt.engine.ui.webadmin.plugin.entity.EntityType::from(Ljava/lang/String;)(entityTypeName);
          };
  
          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);
              },
  
              // TODO(vszocs) inject API functions into "pluginApi.fn" dynamically using EventBus
              addMainTab: function(label, historyToken, contentUrl, options) {
                  if (canDoPluginAction(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 (canDoPluginAction(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));
                  }
              },
  