Hi guys,

I've spent some time working on UI Plugins proof-of-concept (PoC) 
implementation, and thought I'd share my results with you. I've attached a 
patch that reflects the current progress.

The actual PoC implementation takes some inspiration from oVirt UI Plugins wiki 
page, and simplifies/streamlines/improves its main concepts. The goal is to 
have simple-to-use, yet flexible and robust plugin infrastructure. Major 
changes to the original design are outlined below.

Each UI plugin runs within the context of an iframe, and therefore requires a 
plugin source page that executes the actual plugin code.

    • iframe is essentially the sandbox for each plugin. We can disable plugins 
by detaching their iframe elements from the main document during WebAdmin 
runtime. This also allows us to implement features such as plugin safe mode (no 
plugins loaded on WebAdmin startup).
    • Plugin source pages and WebAdmin host page share the same origin 
(protocol, domain, port), with plugin source pages being served through 
EngineManager application server (JBoss AS). This is to avoid cross-domain 
window/iframe communication issues, when the actual plugin code running in an 
iframe tries to register itself into WebAdmin main document's pluginApi object.
    • There's a servlet designed to render plugin source page for all plugins ( 
PluginSourcePageServlet ). For the given plugin, it detects its dependencies 
(3rd party JavaScript libraries) and configuration object (JSON data), reads 
the actual plugin code, and assembles everything into the resulting HTML page 
(to be evaluated by the plugin iframe).
    • iframe isolates plugin dependencies (3rd party JavaScript libraries) from 
other plugins and the main WebAdmin document. In practice, this means that 
plugin A can use jQuery 1.7 and plugin B can use jQuery 1.6 without the fear of 
any clashes.
    • Last but not least, writing plugins in Google Web Toolkit (GWT) should be 
as easy as providing your own plugin source page. Just deploy your GWT plugin 
application on JBoss AS (next to engine.ear ), and point to GWT plugin 
application host page.


The current PoC declares a simple plugin that gets loaded using hard-coded 
values in PluginSourcePageServlet . Actual plugin code registers the plugin 
into global pluginApi.plugins object, with one sample event handler function ( 
ActionButtonClick ). Just after that, the plugin reports in as ready by calling 
pluginApi.ready function. This essentially puts the plugin into use within 
WebAdmin.


To simulate extension point (application event to be consumed by plugins), when 
the user clicks "New server" button on "Virtual Machines" main tab, 
ActionButtonClickEvent gets fired through WebAdmin event bus. 
PluginEventHandler receives this event and invokes ActionButtonClick event 
handler function on all plugins.



(Note: for passing context objects from WebAdmin to plugin event handler 
functions, I'm planning to experiment with gwt-exporter project [1]. This would 
greatly simplify the way how WebAdmin exposes context-specific plugin API to 
event handler functions.)


As for the next step, I suggest to have some meeting (conference) to discuss 
the PoC in detail, and outline tasks for the near future. Also, please let me 
know what you think of the PoC so far.

Cheers,
Vojtech

[1] http://code.google.com/p/gwt-exporter/


From 94c458dc9858ee7220acf53324544b98140c752a Mon Sep 17 00:00:00 2001
From: Vojtech Szocs <vsz...@redhat.com>
Date: Thu, 19 Jul 2012 14:48:40 +0200
Subject: [PATCH] WIP: UI Plugins PoC

Change-Id: Id28812ddbe90574de0178f0c07da713fe9fd8cda
Signed-off-by: Vojtech Szocs <vsz...@redhat.com>
---
 .../server/gwt/PluginSourcePageServlet.java        |   41 ++++++
 .../server/gwt/WebadminDynamicHostingServlet.java  |    4 +
 .../ovirt/engine/ui/webadmin/gin/SystemModule.java |    4 +
 .../ui/webadmin/plugin/PluginApiManager.java       |  143 ++++++++++++++++++++
 .../ui/webadmin/plugin/PluginDefinitions.java      |   30 ++++
 .../ui/webadmin/plugin/PluginEventHandler.java     |   24 ++++
 .../webadmin/plugin/event/ActionButtonClick.java   |    8 +
 .../main/view/tab/MainTabVirtualMachineView.java   |    8 +
 .../webadmin/src/main/webapp/WEB-INF/web.xml       |   10 ++
 9 files changed, 272 insertions(+), 0 deletions(-)
 create mode 100644 frontend/webadmin/modules/frontend/src/main/java/org/ovirt/engine/ui/frontend/server/gwt/PluginSourcePageServlet.java
 create mode 100644 frontend/webadmin/modules/webadmin/src/main/java/org/ovirt/engine/ui/webadmin/plugin/PluginApiManager.java
 create mode 100644 frontend/webadmin/modules/webadmin/src/main/java/org/ovirt/engine/ui/webadmin/plugin/PluginDefinitions.java
 create mode 100644 frontend/webadmin/modules/webadmin/src/main/java/org/ovirt/engine/ui/webadmin/plugin/PluginEventHandler.java
 create mode 100644 frontend/webadmin/modules/webadmin/src/main/java/org/ovirt/engine/ui/webadmin/plugin/event/ActionButtonClick.java

diff --git a/frontend/webadmin/modules/frontend/src/main/java/org/ovirt/engine/ui/frontend/server/gwt/PluginSourcePageServlet.java b/frontend/webadmin/modules/frontend/src/main/java/org/ovirt/engine/ui/frontend/server/gwt/PluginSourcePageServlet.java
new file mode 100644
index 0000000..c408d8e
--- /dev/null
+++ b/frontend/webadmin/modules/frontend/src/main/java/org/ovirt/engine/ui/frontend/server/gwt/PluginSourcePageServlet.java
@@ -0,0 +1,41 @@
+package org.ovirt.engine.ui.frontend.server.gwt;
+
+import java.io.IOException;
+import java.io.PrintWriter;
+
+import javax.servlet.http.HttpServlet;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+
+/**
+ * Renders the HTML source page for the given UI plugin.
+ */
+public class PluginSourcePageServlet extends HttpServlet {
+
+    private static final long serialVersionUID = 1L;
+
+    @Override
+    protected void doGet(HttpServletRequest request, HttpServletResponse response) throws IOException {
+        PrintWriter writer = response.getWriter();
+        response.setContentType("text/html; charset=UTF-8"); //$NON-NLS-1$
+
+        writer.append("<!DOCTYPE html><html><head>"); //$NON-NLS-1$
+        writer.append("<meta http-equiv=\"content-type\" content=\"text/html; charset=UTF-8\">"); //$NON-NLS-1$
+        writer.append("<script type=\"text/javascript\" src=\"https://ajax.googleapis.com/ajax/libs/jquery/1.7.2/jquery.min.js\";></script>"); //$NON-NLS-1$
+        writer.append("</head><body>"); //$NON-NLS-1$
+
+        writer.append("<script type=\"text/javascript\">"); //$NON-NLS-1$
+        writer.append("(function( pluginApi, pluginConfig ) {"); //$NON-NLS-1$
+        writer.append("  window.alert('Invoking actual plugin code!');"); //$NON-NLS-1$
+        writer.append("  window.alert('Reading plugin configuration: ' + pluginConfig.foo);"); //$NON-NLS-1$
+        writer.append("  pluginApi.plugins['myPlugin'] = {"); //$NON-NLS-1$
+        writer.append("    ActionButtonClick: function(contextObject) { window.alert('Woohoo!'); }"); //$NON-NLS-1$
+        writer.append("  };"); //$NON-NLS-1$
+        writer.append("  pluginApi.ready('myPlugin');"); //$NON-NLS-1$
+        writer.append("}) ( parent.pluginApi, { \"foo\": 123 } );"); //$NON-NLS-1$
+        writer.append("</script>"); //$NON-NLS-1$
+
+        writer.append("</body></html>"); //$NON-NLS-1$
+    }
+
+}
diff --git a/frontend/webadmin/modules/frontend/src/main/java/org/ovirt/engine/ui/frontend/server/gwt/WebadminDynamicHostingServlet.java b/frontend/webadmin/modules/frontend/src/main/java/org/ovirt/engine/ui/frontend/server/gwt/WebadminDynamicHostingServlet.java
index 428dcc5..dcaf49a 100644
--- a/frontend/webadmin/modules/frontend/src/main/java/org/ovirt/engine/ui/frontend/server/gwt/WebadminDynamicHostingServlet.java
+++ b/frontend/webadmin/modules/frontend/src/main/java/org/ovirt/engine/ui/frontend/server/gwt/WebadminDynamicHostingServlet.java
@@ -36,6 +36,10 @@ public class WebadminDynamicHostingServlet extends GwtDynamicHostPageServlet {
             appModeData.put("value", String.valueOf(applicationMode)); //$NON-NLS-1$
             writeJsObject(writer, "applicationMode", appModeData); //$NON-NLS-1$
         }
+
+        Map<String, String> pluginDefinitions = new HashMap<String, String>();
+        pluginDefinitions.put("myPlugin", "/webadmin/webadmin/PluginSourcePage?plugin=myPlugin"); //$NON-NLS-1$ //$NON-NLS-2$
+        writeJsObject(writer, "pluginDefinitions", pluginDefinitions); //$NON-NLS-1$
     }
 
     private Integer getApplicationMode(HttpServletRequest request) {
diff --git a/frontend/webadmin/modules/webadmin/src/main/java/org/ovirt/engine/ui/webadmin/gin/SystemModule.java b/frontend/webadmin/modules/webadmin/src/main/java/org/ovirt/engine/ui/webadmin/gin/SystemModule.java
index 3069b99..78963ad 100644
--- a/frontend/webadmin/modules/webadmin/src/main/java/org/ovirt/engine/ui/webadmin/gin/SystemModule.java
+++ b/frontend/webadmin/modules/webadmin/src/main/java/org/ovirt/engine/ui/webadmin/gin/SystemModule.java
@@ -9,6 +9,8 @@ import org.ovirt.engine.ui.webadmin.ApplicationResources;
 import org.ovirt.engine.ui.webadmin.ApplicationTemplates;
 import org.ovirt.engine.ui.webadmin.place.ApplicationPlaces;
 import org.ovirt.engine.ui.webadmin.place.WebAdminPlaceManager;
+import org.ovirt.engine.ui.webadmin.plugin.PluginApiManager;
+import org.ovirt.engine.ui.webadmin.plugin.PluginEventHandler;
 import org.ovirt.engine.ui.webadmin.system.ApplicationInit;
 import org.ovirt.engine.ui.webadmin.system.InternalConfiguration;
 
@@ -31,6 +33,8 @@ public class SystemModule extends BaseSystemModule {
         bind(PlaceManager.class).to(WebAdminPlaceManager.class).in(Singleton.class);
         bind(ApplicationInit.class).asEagerSingleton();
         bind(InternalConfiguration.class).asEagerSingleton();
+        bind(PluginApiManager.class).asEagerSingleton();
+        bind(PluginEventHandler.class).asEagerSingleton();
     }
 
     void bindConfiguration() {
diff --git a/frontend/webadmin/modules/webadmin/src/main/java/org/ovirt/engine/ui/webadmin/plugin/PluginApiManager.java b/frontend/webadmin/modules/webadmin/src/main/java/org/ovirt/engine/ui/webadmin/plugin/PluginApiManager.java
new file mode 100644
index 0000000..0bbdd68
--- /dev/null
+++ b/frontend/webadmin/modules/webadmin/src/main/java/org/ovirt/engine/ui/webadmin/plugin/PluginApiManager.java
@@ -0,0 +1,143 @@
+package org.ovirt.engine.ui.webadmin.plugin;
+
+import java.util.HashMap;
+import java.util.Map;
+import java.util.logging.Logger;
+
+import com.google.gwt.core.client.JavaScriptObject;
+import com.google.gwt.core.client.JsArrayString;
+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;
+
+/**
+ * The main component of WebAdmin UI plugin infrastructure.
+ * <p>
+ * Should be bound as GIN eager singleton, created early on during application startup.
+ */
+public class PluginApiManager {
+
+    private static final Logger logger = Logger.getLogger(PluginApiManager.class.getName());
+
+    // Maps plugin names to corresponding iframe elements
+    private final Map<String, IFrameElement> pluginIFrames = new HashMap<String, IFrameElement>();
+
+    // Maps plugin names to corresponding plugin objects (only for plugins which are currently ready)
+    private final Map<String, JavaScriptObject> pluginObjects = new HashMap<String, JavaScriptObject>();
+
+    public PluginApiManager() {
+        exposePluginApi();
+        loadPlugins();
+    }
+
+    /**
+     * Invokes the given event handler function on all plugins which are currently ready.
+     */
+    public void invokePlugins(String functionName, JavaScriptObject contextObject) {
+        for (JavaScriptObject pluginObject : pluginObjects.values()) {
+            invokePlugin(pluginObject, functionName, contextObject);
+        }
+    }
+
+    native void invokePlugin(JavaScriptObject pluginObject, String functionName, JavaScriptObject contextObject) /*-{
+        if (contextObject != null) {
+            pluginObject[functionName](contextObject);
+        } else {
+            pluginObject[functionName]();
+        }
+    }-*/;
+
+    /**
+     * Loads all plugins that were detected when serving WebAdmin host page.
+     */
+    void loadPlugins() {
+        PluginDefinitions defs = PluginDefinitions.instance();
+
+        if (defs != null) {
+            JsArrayString pluginNames = defs.getPluginNames();
+
+            for (int i = 0; i < pluginNames.length(); i++) {
+                String name = pluginNames.get(i);
+                String sourcePageUrl = defs.getPluginSourcePageUrl(name);
+
+                logger.info("Loading plugin [" + name + "] from URL " + sourcePageUrl); //$NON-NLS-1$ //$NON-NLS-2$
+                loadPlugin(name, sourcePageUrl);
+            }
+        }
+    }
+
+    /**
+     * Loads a plugin using its source page (HTML page that executes the actual plugin code).
+     * <p>
+     * WebAdmin requires all plugins to have a source page because each plugin runs within the context of an iframe.
+     * <p>
+     * Note that plugin source page URLs are always relative to JBoss server root path. Source page URLs must therefore
+     * always begin with slash ({@code /}) character. This allows WebAdmin host page and individual plugin source pages
+     * to share the same origin (protocol, domain, port) and avoid cross-domain window/iframe communication issues.
+     */
+    void loadPlugin(String pluginName, String pluginSourcePageUrl) {
+        if (!pluginSourcePageUrl.startsWith("/")) { //$NON-NLS-1$
+            logger.warning("Attempting to load plugin [" + pluginName + "] using invalid URL " + pluginSourcePageUrl); //$NON-NLS-1$ //$NON-NLS-2$
+            return;
+        }
+
+        if (pluginIFrames.containsKey(pluginName)) {
+            logger.warning("Plugin [" + pluginName + "] is already loaded"); //$NON-NLS-1$ //$NON-NLS-2$
+            return;
+        }
+
+        // Create an iframe used to load the plugin source page
+        IFrameElement iframe = Document.get().createIFrameElement();
+        iframe.setSrc(pluginSourcePageUrl);
+        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);
+        pluginIFrames.put(pluginName, iframe);
+
+        // Attach the iframe to DOM document body
+        Document.get().getBody().appendChild(iframe);
+    }
+
+    /**
+     * Indicates that the given plugin is ready for use.
+     * <p>
+     * Note that event handler functions will be invoked only on plugins which are currently ready.
+     */
+    void pluginReady(String pluginName, JavaScriptObject pluginObject) {
+        if (pluginName == null) {
+            logger.warning("Plugin name is null or undefined"); //$NON-NLS-1$
+            return;
+        }
+
+        if (!pluginIFrames.containsKey(pluginName)) {
+            logger.warning("Plugin [" + pluginName + "] reports in as ready, but has no iframe associated"); //$NON-NLS-1$ //$NON-NLS-2$
+            return;
+        }
+
+        pluginObjects.put(pluginName, pluginObject);
+    }
+
+    native void exposePluginApi() /*-{
+        var instance = this;
+
+        // Expose the global pluginApi object
+        $wnd.pluginApi = {
+
+            // Plugins will register themselves into this object by adding new property:
+            // - property name is the name of the plugin
+            // - property value is the plugin object containing event handler functions
+            plugins: {},
+
+            ready: function(pluginName) {
+                var pluginObject = this.plugins[pluginName];
+                instan...@org.ovirt.engine.ui.webadmin.plugin.PluginApiManager::pluginReady(Ljava/lang/String;Lcom/google/gwt/core/client/JavaScriptObject;)(pluginName,pluginObject);
+            }
+
+        };
+    }-*/;
+
+}
diff --git a/frontend/webadmin/modules/webadmin/src/main/java/org/ovirt/engine/ui/webadmin/plugin/PluginDefinitions.java b/frontend/webadmin/modules/webadmin/src/main/java/org/ovirt/engine/ui/webadmin/plugin/PluginDefinitions.java
new file mode 100644
index 0000000..d1118cb
--- /dev/null
+++ b/frontend/webadmin/modules/webadmin/src/main/java/org/ovirt/engine/ui/webadmin/plugin/PluginDefinitions.java
@@ -0,0 +1,30 @@
+package org.ovirt.engine.ui.webadmin.plugin;
+
+import com.google.gwt.core.client.JavaScriptObject;
+import com.google.gwt.core.client.JsArrayString;
+
+/**
+ * Overlay type for {@code pluginDefinitions} global JS object.
+ */
+public final class PluginDefinitions extends JavaScriptObject {
+
+    protected PluginDefinitions() {
+    }
+
+    public static native PluginDefinitions instance() /*-{
+        return $wnd.pluginDefinitions;
+    }-*/;
+
+    public native JsArrayString getPluginNames() /*-{
+        var pluginNames = [];
+        for (var key in this) {
+            pluginNames.push(key);
+        }
+        return pluginNames;
+    }-*/;
+
+    public native String getPluginSourcePageUrl(String pluginName) /*-{
+        return this[pluginName];
+    }-*/;
+
+}
diff --git a/frontend/webadmin/modules/webadmin/src/main/java/org/ovirt/engine/ui/webadmin/plugin/PluginEventHandler.java b/frontend/webadmin/modules/webadmin/src/main/java/org/ovirt/engine/ui/webadmin/plugin/PluginEventHandler.java
new file mode 100644
index 0000000..9f501ac
--- /dev/null
+++ b/frontend/webadmin/modules/webadmin/src/main/java/org/ovirt/engine/ui/webadmin/plugin/PluginEventHandler.java
@@ -0,0 +1,24 @@
+package org.ovirt.engine.ui.webadmin.plugin;
+
+import org.ovirt.engine.ui.webadmin.plugin.event.ActionButtonClickEvent;
+import org.ovirt.engine.ui.webadmin.plugin.event.ActionButtonClickEvent.ActionButtonClickHandler;
+
+import com.google.gwt.event.shared.EventBus;
+import com.google.inject.Inject;
+
+/**
+ * Handles WebAdmin application events to be consumed by UI plugins.
+ */
+public class PluginEventHandler {
+
+    @Inject
+    public PluginEventHandler(EventBus eventBus, final PluginApiManager manager) {
+        eventBus.addHandler(ActionButtonClickEvent.getType(), new ActionButtonClickHandler() {
+            @Override
+            public void onActionButtonClick(ActionButtonClickEvent event) {
+                manager.invokePlugins("ActionButtonClick", null); //$NON-NLS-1$
+            }
+        });
+    }
+
+}
diff --git a/frontend/webadmin/modules/webadmin/src/main/java/org/ovirt/engine/ui/webadmin/plugin/event/ActionButtonClick.java b/frontend/webadmin/modules/webadmin/src/main/java/org/ovirt/engine/ui/webadmin/plugin/event/ActionButtonClick.java
new file mode 100644
index 0000000..6145b3d
--- /dev/null
+++ b/frontend/webadmin/modules/webadmin/src/main/java/org/ovirt/engine/ui/webadmin/plugin/event/ActionButtonClick.java
@@ -0,0 +1,8 @@
+package org.ovirt.engine.ui.webadmin.plugin.event;
+
+import com.gwtplatform.dispatch.annotation.GenEvent;
+
+@GenEvent
+public class ActionButtonClick {
+
+}
diff --git a/frontend/webadmin/modules/webadmin/src/main/java/org/ovirt/engine/ui/webadmin/section/main/view/tab/MainTabVirtualMachineView.java b/frontend/webadmin/modules/webadmin/src/main/java/org/ovirt/engine/ui/webadmin/section/main/view/tab/MainTabVirtualMachineView.java
index 8788cc7..018d31c 100644
--- a/frontend/webadmin/modules/webadmin/src/main/java/org/ovirt/engine/ui/webadmin/section/main/view/tab/MainTabVirtualMachineView.java
+++ b/frontend/webadmin/modules/webadmin/src/main/java/org/ovirt/engine/ui/webadmin/section/main/view/tab/MainTabVirtualMachineView.java
@@ -17,6 +17,8 @@ import org.ovirt.engine.ui.uicommonweb.models.vms.ConsoleModel;
 import org.ovirt.engine.ui.uicommonweb.models.vms.VmListModel;
 import org.ovirt.engine.ui.webadmin.ApplicationConstants;
 import org.ovirt.engine.ui.webadmin.ApplicationResources;
+import org.ovirt.engine.ui.webadmin.gin.ClientGinjectorProvider;
+import org.ovirt.engine.ui.webadmin.plugin.event.ActionButtonClickEvent;
 import org.ovirt.engine.ui.webadmin.section.main.presenter.tab.MainTabVirtualMachinePresenter;
 import org.ovirt.engine.ui.webadmin.section.main.view.AbstractMainTabWithDetailsTableView;
 import org.ovirt.engine.ui.webadmin.uicommon.ReportActionsHelper;
@@ -163,6 +165,12 @@ public class MainTabVirtualMachineView extends AbstractMainTabWithDetailsTableVi
             protected UICommand resolveCommand() {
                 return getMainModel().getNewServerCommand();
             }
+
+            @Override
+            public void onClick(List<VM> selectedItems) {
+                super.onClick(selectedItems);
+                ActionButtonClickEvent.fire(ClientGinjectorProvider.instance().getEventBus());
+            }
         });
         getTable().addActionButton(new WebAdminButtonDefinition<VM>(constants.newDesktopVm()) {
             @Override
diff --git a/frontend/webadmin/modules/webadmin/src/main/webapp/WEB-INF/web.xml b/frontend/webadmin/modules/webadmin/src/main/webapp/WEB-INF/web.xml
index a50f8aa..62f0312 100644
--- a/frontend/webadmin/modules/webadmin/src/main/webapp/WEB-INF/web.xml
+++ b/frontend/webadmin/modules/webadmin/src/main/webapp/WEB-INF/web.xml
@@ -23,6 +23,16 @@
 		<url-pattern>/webadmin/WebAdmin.html</url-pattern>
 	</servlet-mapping>
 
+	<servlet>
+		<servlet-name>PluginSourcePage</servlet-name>
+		<servlet-class>org.ovirt.engine.ui.frontend.server.gwt.PluginSourcePageServlet</servlet-class>
+	</servlet>
+
+	<servlet-mapping>
+		<servlet-name>PluginSourcePage</servlet-name>
+		<url-pattern>/webadmin/PluginSourcePage</url-pattern>
+	</servlet-mapping>
+
 	<!-- Default page to serve -->
 	<welcome-file-list>
 		<welcome-file>index.html</welcome-file>
-- 
1.7.4.4

_______________________________________________
Engine-devel mailing list
Engine-devel@ovirt.org
http://lists.ovirt.org/mailman/listinfo/engine-devel

Reply via email to