FELIX-1485: Adding AdminService support to the web console

git-svn-id: https://svn.apache.org/repos/asf/felix/trunk@804868 13f79535-47bb-0310-9956-ffa450edef68
diff --git a/karaf/assembly/src/main/filtered-resources/features.xml b/karaf/assembly/src/main/filtered-resources/features.xml
index f18ba6a..64d20c5 100644
--- a/karaf/assembly/src/main/filtered-resources/features.xml
+++ b/karaf/assembly/src/main/filtered-resources/features.xml
@@ -60,6 +60,7 @@
         <!-- TODO: uncomment when FELIX-1133 is resolved
         <bundle>mvn:org.apache.felix.karaf.webconsole/org.apache.felix.karaf.webconsole.branding/${version}</bundle>
         -->
+        <bundle>mvn:org.apache.felix.karaf.webconsole/org.apache.felix.karaf.webconsole.admin/${version}</bundle>
         <bundle>mvn:org.apache.felix.karaf.webconsole/org.apache.felix.karaf.webconsole.features/${version}</bundle>
         <bundle>mvn:org.apache.felix.karaf.webconsole/org.apache.felix.karaf.webconsole.gogo/${version}</bundle>
     </feature>
diff --git a/karaf/gshell/gshell-admin/src/main/resources/OSGI-INF/blueprint/gshell-admin.xml b/karaf/gshell/gshell-admin/src/main/resources/OSGI-INF/blueprint/gshell-admin.xml
index 74cad8b..d92260a 100644
--- a/karaf/gshell/gshell-admin/src/main/resources/OSGI-INF/blueprint/gshell-admin.xml
+++ b/karaf/gshell/gshell-admin/src/main/resources/OSGI-INF/blueprint/gshell-admin.xml
@@ -87,5 +87,7 @@
     <bean id="instanceCompleter" class="org.apache.felix.karaf.gshell.admin.internal.completers.InstanceCompleter">
         <property name="adminService" ref="adminService" />
     </bean>
+    
+    <service ref="adminService" interface="org.apache.felix.karaf.gshell.admin.AdminService" />
 
 </blueprint>
diff --git a/karaf/webconsole/admin/pom.xml b/karaf/webconsole/admin/pom.xml
new file mode 100644
index 0000000..a47f66c
--- /dev/null
+++ b/karaf/webconsole/admin/pom.xml
@@ -0,0 +1,97 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
+
+    <!--
+
+        Licensed to the Apache Software Foundation (ASF) under one or more
+        contributor license agreements.  See the NOTICE file distributed with
+        this work for additional information regarding copyright ownership.
+        The ASF licenses this file to You under the Apache License, Version 2.0
+        (the "License"); you may not use this file except in compliance with
+        the License.  You may obtain a copy of the License at
+
+           http://www.apache.org/licenses/LICENSE-2.0
+
+        Unless required by applicable law or agreed to in writing, software
+        distributed under the License is distributed on an "AS IS" BASIS,
+        WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+        See the License for the specific language governing permissions and
+        limitations under the License.
+    -->
+
+    <modelVersion>4.0.0</modelVersion>
+  
+    <parent>
+        <groupId>org.apache.felix.karaf.webconsole</groupId>
+        <artifactId>webconsole</artifactId>
+        <version>1.2.0-SNAPSHOT</version>
+    </parent>
+
+    <groupId>org.apache.felix.karaf.webconsole</groupId>
+    <artifactId>org.apache.felix.karaf.webconsole.admin</artifactId>
+    <packaging>bundle</packaging>
+    <version>1.2.0-SNAPSHOT</version>
+    <name>Apache Felix Karaf :: Web Console :: Admin Plugin</name>
+  
+    <dependencies>
+        <dependency>
+            <groupId>org.apache.felix</groupId>
+            <artifactId>org.osgi.core</artifactId>
+            <scope>provided</scope>
+        </dependency>
+        <dependency>
+            <groupId>org.apache.felix</groupId>
+            <artifactId>org.osgi.compendium</artifactId>
+            <scope>provided</scope>
+        </dependency>
+        <dependency>
+            <groupId>org.apache.felix</groupId>
+            <artifactId>org.apache.felix.webconsole</artifactId>
+            <scope>provided</scope>
+        </dependency>
+        <dependency>
+            <groupId>javax.servlet</groupId>
+            <artifactId>servlet-api</artifactId>
+            <scope>provided</scope>
+        </dependency>
+        <dependency>
+            <groupId>org.apache.felix.karaf.gshell</groupId>
+            <artifactId>org.apache.felix.karaf.gshell.admin</artifactId>
+            <version>1.2.0-SNAPSHOT</version>
+            <scope>provided</scope>
+        </dependency>
+        <dependency>
+            <groupId>commons-logging</groupId>
+            <artifactId>commons-logging</artifactId>
+            <scope>provided</scope>
+        </dependency>
+        <dependency>
+            <groupId>org.json</groupId>
+            <artifactId>json</artifactId>
+            <version>20070829</version>
+            <scope>compile</scope>
+            <optional>true</optional>
+        </dependency>
+    </dependencies>
+  
+    <build>
+    <plugins>
+      <plugin>
+        <groupId>org.apache.felix</groupId>
+        <artifactId>maven-bundle-plugin</artifactId>
+        <configuration>
+          <instructions>
+            <Bundle-SymbolicName>${project.artifactId}</Bundle-SymbolicName>
+            <Export-Package>org.apache.felix.karaf.webconsole.admin;version=${pom.version}</Export-Package>
+            <Embed-Dependency>
+               <!-- Required for JSON data transfer -->
+               <!-- TODO: this needs to be put in a common place for reuse. -->
+               json
+            </Embed-Dependency>
+          </instructions>
+        </configuration>
+      </plugin>
+    </plugins>
+  </build>
+  
+</project>
diff --git a/karaf/webconsole/admin/src/main/java/org/apache/felix/karaf/webconsole/admin/AdminPlugin.java b/karaf/webconsole/admin/src/main/java/org/apache/felix/karaf/webconsole/admin/AdminPlugin.java
new file mode 100644
index 0000000..f3d62ac
--- /dev/null
+++ b/karaf/webconsole/admin/src/main/java/org/apache/felix/karaf/webconsole/admin/AdminPlugin.java
@@ -0,0 +1,334 @@
+/*
+ *  Copyright 2009 Marcin.
+ * 
+ *  Licensed under the Apache License, Version 2.0 (the "License");
+ *  you may not use this file except in compliance with the License.
+ *  You may obtain a copy of the License at
+ * 
+ *       http://www.apache.org/licenses/LICENSE-2.0
+ * 
+ *  Unless required by applicable law or agreed to in writing, software
+ *  distributed under the License is distributed on an "AS IS" BASIS,
+ *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ *  See the License for the specific language governing permissions and
+ *  limitations under the License.
+ *  under the License.
+ */
+package org.apache.felix.karaf.webconsole.admin;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.PrintWriter;
+import java.net.URL;
+import java.util.logging.Level;
+import java.util.logging.Logger;
+import javax.servlet.ServletException;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+import org.apache.felix.karaf.gshell.admin.AdminService;
+import org.apache.felix.karaf.gshell.admin.Instance;
+import org.apache.felix.webconsole.AbstractWebConsolePlugin;
+import org.json.JSONException;
+import org.osgi.framework.BundleContext;
+import org.apache.commons.logging.Log;
+import org.apache.commons.logging.LogFactory;
+import org.json.JSONWriter;
+
+/**
+ * Felix Web Console plugin for interacting with the {@link AdminService}
+ */
+public class AdminPlugin extends AbstractWebConsolePlugin {
+
+    public static final String NAME = "admin";
+    public static final String LABEL = "Admin";
+    private String adminJs = "/admin/res/ui/admin.js";
+    private BundleContext bundleContext;
+    private AdminService adminService;
+    private Log log = LogFactory.getLog(AdminPlugin.class);
+    private ClassLoader classLoader;
+
+    /**
+     * Blueprint lifecycle callback methods
+     */
+    public void start() {
+        super.activate(bundleContext);
+        this.classLoader = this.getClass().getClassLoader();
+        this.log.info(LABEL + " plugin activated");
+    }
+
+    public void stop() {
+        this.log.info(LABEL + " plugin deactivated");
+        super.deactivate();
+    }
+
+    @Override
+    public String getTitle() {
+        return LABEL;
+    }
+
+    @Override
+    public String getLabel() {
+        return NAME;
+    }
+
+    @Override
+    protected void renderContent(HttpServletRequest req, HttpServletResponse res) throws ServletException, IOException {
+        final PrintWriter pw = res.getWriter();
+
+        String appRoot = (String) req.getAttribute("org.apache.felix.webconsole.internal.servlet.OsgiManager.appRoot");
+        final String adminScriptTag = "<script src='" + appRoot + this.adminJs + "' language='JavaScript'></script>";
+        pw.println(adminScriptTag);
+
+        pw.println("<script type='text/javascript'>");
+        pw.println("// <![CDATA[");
+        pw.println("var imgRoot = '" + appRoot + "/res/imgs';");
+        pw.println("// ]]>");
+        pw.println("</script>");
+
+        pw.println("<div id='plugin_content'/>");
+
+        pw.println("<script type='text/javascript'>");
+        pw.println("// <![CDATA[");
+        pw.print("renderAdmin( ");
+        writeJSON(pw);
+        pw.println(" )");
+        pw.println("// ]]>");
+        pw.println("</script>");
+    }
+
+    @Override
+    protected void doPost(HttpServletRequest req, HttpServletResponse res) throws ServletException, IOException {
+        boolean success = false;
+
+        String action = req.getParameter("action");
+        String name = req.getParameter("name");
+
+        if (action == null) {
+            success = true;
+        } else if ("create".equals(action)) {
+            int port = parsePortNumber(req.getParameter("port"));
+            String location = parseString(req.getParameter("location"));
+            success = createInstance(name, port, location);
+        } else if ("destroy".equals(action)) {
+            success = destroyInstance(name);
+        } else if ("start".equals(action)) {
+            String javaOpts = req.getParameter("javaOpts");
+            success = startInstance(name, javaOpts);
+        } else if ("stop".equals(action)) {
+            success = stopInstance(name);
+        }
+
+        if (success) {
+            try {
+                Thread.sleep(1000);
+            } catch (InterruptedException e) {
+            }
+            this.renderJSON(res, null);
+        } else {
+            super.doPost(req, res);
+        }
+    }
+
+    /*
+     * Parse the String value, returning <code>null</code> if the String is empty 
+     */
+    private String parseString(String value) {
+        if (value != null && value.trim().length() == 0) {
+            value = null;
+        }
+        return value;
+    }
+
+    /*
+     * Parse the port number for the String given, returning 0 if the String does not represent an integer 
+     */
+    private int parsePortNumber(String port) {
+        try {
+            return Integer.parseInt(port);
+        } catch (NumberFormatException e) {
+            return 0;
+        }
+    }
+
+    protected URL getResource(String path) {
+        path = path.substring(NAME.length() + 1);
+        URL url = this.classLoader.getResource(path);
+        try {
+            InputStream ins = url.openStream();
+            if (ins == null) {
+                this.log.error("failed to open " + url);
+            }
+        } catch (IOException e) {
+            this.log.error(e.getMessage(), e);
+        }
+        return url;
+    }
+
+    private void renderJSON(final HttpServletResponse response, final String feature) throws IOException {
+        response.setContentType("application/json");
+        response.setCharacterEncoding("UTF-8");
+
+        final PrintWriter pw = response.getWriter();
+        writeJSON(pw);
+    }
+
+    private void writeJSON(final PrintWriter pw) {
+        final JSONWriter jw = new JSONWriter(pw);
+        final Instance[] instances = adminService.getInstances();
+        try {
+            jw.object();
+            jw.key("status");
+            jw.value(getStatusLine());
+            jw.key("instances");
+            jw.array();
+            for (Instance i : instances) {
+                instanceInfo(jw, i);
+            }
+            jw.endArray();
+            jw.endObject();
+        } catch (JSONException ex) {
+            Logger.getLogger(AdminPlugin.class.getName()).log(Level.SEVERE, null, ex);
+        } catch (Exception ex) {
+            Logger.getLogger(AdminPlugin.class.getName()).log(Level.SEVERE, null, ex);
+        }
+    }
+
+    private void instanceInfo(JSONWriter jw, Instance instance) throws JSONException, Exception {
+        jw.object();
+        jw.key("pid");
+        jw.value(instance.getPid());
+        jw.key("name");
+        jw.value(instance.getName());
+        jw.key("port");
+        jw.value(instance.getPort());
+        jw.key("state");
+        jw.value(instance.getState());
+        jw.key("location");
+        jw.value(instance.getLocation());
+        jw.key("actions");
+        jw.array();
+        action(jw, "destroy", "Destroy", "delete");
+        if (instance.getState().equals(Instance.STARTED)) {
+            action(jw, "stop", "Stop", "stop");
+        } else if (instance.getState().equals(Instance.STARTING)) {
+            action(jw, "stop", "Stop", "stop");
+        } else if (instance.getState().equals(Instance.STOPPED)) {
+            action(jw, "start", "Start", "start");
+        }
+        jw.endArray();
+        jw.endObject();
+    }
+
+    private void action(JSONWriter jw, String op, String title, String image) throws JSONException {
+        jw.object();
+        jw.key("op").value(op);
+        jw.key("title").value(title);
+        jw.key("image").value(image);
+        jw.endObject();
+    }
+
+    private String getStatusLine() {
+        final Instance[] instances = adminService.getInstances();
+        int started = 0, starting = 0, stopped = 0;
+        for (Instance instance : instances) {
+            try {
+                if (instance.getState().equals(Instance.STARTED)) {
+                    started++;
+                } else if (instance.getState().equals(Instance.STARTING)) {
+                    starting++;
+                } else if (instance.getState().equals(Instance.STOPPED)) {
+                    stopped++;
+                }
+            } catch (Exception ex) {
+                Logger.getLogger(AdminPlugin.class.getName()).log(Level.SEVERE, null, ex);
+            }
+        }
+        final StringBuffer buffer = new StringBuffer();
+        buffer.append("Instance information: ");
+        buffer.append(instances.length);
+        buffer.append(" instance");
+        if (instances.length != 1) {
+            buffer.append('s');
+        }
+        buffer.append(" in total");
+        if (started == instances.length) {
+            buffer.append(" - all started");
+        } else {
+            if (started != 0) {
+                buffer.append(", ");
+                buffer.append(started);
+                buffer.append(" started");
+            }
+            if (starting != 0) {
+                buffer.append(", ");
+                buffer.append(starting);
+                buffer.append(" starting");
+            }
+            buffer.append('.');
+        }
+        return buffer.toString();
+    }
+
+    private boolean createInstance(String name, int port, String location) {
+        try {
+            adminService.createInstance(name, port, location);
+            return true;
+        } catch (Exception ex) {
+            Logger.getLogger(AdminPlugin.class.getName()).log(Level.SEVERE, null, ex);
+        }
+        return false;
+    }
+
+    private boolean destroyInstance(String name) {
+        try {
+            Instance instance = adminService.getInstance(name);
+            if (instance != null) {
+                instance.destroy();
+                return true;
+            }
+        } catch (Exception ex) {
+            Logger.getLogger(AdminPlugin.class.getName()).log(Level.SEVERE, null, ex);
+        }
+        return false;
+    }
+
+    private boolean startInstance(String name, String javaOpts) {
+        try {
+            Instance instance = adminService.getInstance(name);
+            if (instance != null) {
+                instance.start(javaOpts);
+                return true;
+            }
+        } catch (Exception ex) {
+            Logger.getLogger(AdminPlugin.class.getName()).log(Level.SEVERE, null, ex);
+        }
+        return false;
+    }
+
+    private boolean stopInstance(String name) {
+        try {
+            Instance instance = adminService.getInstance(name);
+            if (instance != null) {
+                instance.stop();
+                return true;
+            }
+        } catch (Exception ex) {
+            Logger.getLogger(AdminPlugin.class.getName()).log(Level.SEVERE, null, ex);
+        }
+        return false;
+    }
+
+    /**
+     * @param adminService the adminService to set
+     */
+    public void setAdminService(AdminService adminService) {
+        this.adminService = adminService;
+    }
+
+    /**
+     * @param bundleContext the bundleContext to set
+     */
+    public void setBundleContext(BundleContext bundleContext) {
+        this.bundleContext = bundleContext;
+    }
+}
diff --git a/karaf/webconsole/admin/src/main/resources/OSGI-INF/blueprint/webconsole-admin.xml b/karaf/webconsole/admin/src/main/resources/OSGI-INF/blueprint/webconsole-admin.xml
new file mode 100644
index 0000000..9d65b92
--- /dev/null
+++ b/karaf/webconsole/admin/src/main/resources/OSGI-INF/blueprint/webconsole-admin.xml
@@ -0,0 +1,36 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+
+    Licensed to the Apache Software Foundation (ASF) under one or more
+    contributor license agreements.  See the NOTICE file distributed with
+    this work for additional information regarding copyright ownership.
+    The ASF licenses this file to You under the Apache License, Version 2.0
+    (the "License"); you may not use this file except in compliance with
+    the License.  You may obtain a copy of the License at
+
+       http://www.apache.org/licenses/LICENSE-2.0
+
+    Unless required by applicable law or agreed to in writing, software
+    distributed under the License is distributed on an "AS IS" BASIS,
+    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+    See the License for the specific language governing permissions and
+    limitations under the License.
+
+-->
+<blueprint xmlns="http://www.osgi.org/xmlns/blueprint/v1.0.0"
+           xmlns:cm="http://www.osgi.org/xmlns/blueprint-cm/v1.0.0">
+
+    <reference id="adminService" interface="org.apache.felix.karaf.gshell.admin.AdminService" />
+
+    <bean id="adminPlugin" class="org.apache.felix.karaf.webconsole.admin.AdminPlugin" init-method="start" destroy-method="stop">
+        <property name="adminService" ref="adminService" />
+        <property name="bundleContext" ref="blueprintBundleContext" />
+    </bean>
+
+    <service ref="adminPlugin" interface="javax.servlet.Servlet" >
+        <service-properties>
+            <entry key="felix.webconsole.label" value="admin"/>
+        </service-properties>
+    </service>
+
+</blueprint>
diff --git a/karaf/webconsole/admin/src/main/resources/res/ui/admin.js b/karaf/webconsole/admin/src/main/resources/res/ui/admin.js
new file mode 100644
index 0000000..caf62e0
--- /dev/null
+++ b/karaf/webconsole/admin/src/main/resources/res/ui/admin.js
@@ -0,0 +1,142 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+function renderAdmin( data ) {
+    $(document).ready( function() {
+        renderView();
+        renderData( data );
+    } );
+}
+
+function renderView() {
+    renderStatusLine();
+    var txt = "<form method='post'><div class='table'><table id='create_instance_table' class='tablelayout'><tbody>" +
+    "<tr><input type='hidden' name='action' value='create'/>" +
+    "<td>Name: <input id='name' type='text' name='name' style='width:70%' colspan='2'/></td>" +
+    "<td>Port: <input id='port' type='text' name='port' style='width:70%' colspan='2'/></td>" +
+    "<td>Location: <input id='location' type='text' name='location' style='width:70%' colspan='2'/></td>" +
+    "<td class='col_Actions'><input type='button' value='Create' onclick='createInstance()'/></td>" +
+    "</tr></tbody></table></div></form><br/>";
+    $("#plugin_content").append( txt );
+    renderTable( "Karaf Instances", "instances_table", ["Pid", "Name", "Port", "State", "Location", "Actions"] );
+    renderStatusLine();
+}
+
+function createInstance() {
+    var name = document.getElementById( "name" ).value;
+    var port = document.getElementById( "port" ).value;
+    var location = document.getElementById( "location" ).value;
+    postCreateInstance( name, port, location );
+}
+
+function postCreateInstance( /* String */ name, /* String */ port, /* String */ location ) {
+    $.post( pluginRoot, {"action": "create", "name": name, "port": port, "location": location}, function( data ) {
+        renderData( data );
+    }, "json" );
+}
+
+function renderStatusLine() {
+    $("#plugin_content").append( "<div class='fullwidth'><div class='statusline'/></div>" );
+}
+
+function renderTable( /* String */ title, /* String */ id, /* array of Strings */ columns ) {
+    var txt = "<div class='table'><table class='tablelayout'><tbody><tr>" +
+    "<td style='color:#6181A9;background-color:#e6eeee'>" +
+    title + "</td></tr></tbody></table>" +
+    "<table id='" + id + "' class='tablelayout'><thead><tr>";
+    for ( var name in columns ) {
+        txt = txt + "<th class='col_" + columns[name] + "' style='border-top:#e6eeee'>" + columns[name] + "</th>";
+    }
+    txt = txt + "</tr></thead><tbody></tbody></table></div>";
+    $("#plugin_content").append( txt );
+}
+
+function renderData( /* Object */ data ) {
+    renderStatusData( data.status );
+    renderInstancesTableData( data.instances );
+    $("#instances_table").tablesorter( {
+        headers: {
+            5: { 
+                sorter: false
+            }
+        },
+        sortList: [[0,0]],
+    } );
+}
+
+function renderStatusData( /* String */ status )  {
+    $(".statusline").empty().append( status );
+}
+
+function renderInstancesTableData( /* array of Objects */ instances ) {
+    $("#instances_table > tbody > tr").remove();
+    for ( var idx in instances ) {
+        var trElement = tr( null, { 
+            id: instances[idx].pid
+        } );
+        renderInstanceData( trElement, instances[idx] );
+        $("#instances_table > tbody").append( trElement );
+    }
+    $("#instances_table").trigger( "update" );
+}
+
+function renderInstanceData( /* Element */ parent, /* Object */ instance ) {
+    parent.appendChild( td( null, null, [ text( instance.pid ) ] ) );
+    parent.appendChild( td( null, null, [ text( instance.name ) ] ) );
+    parent.appendChild( td( null, null, [ text( instance.port ) ] ) );
+    parent.appendChild( td( null, null, [ text( instance.state ) ] ) );
+    parent.appendChild( td( null, null, [ text( instance.location ) ] ) );
+    var actionsTd = td( null, null );
+    var div = createElement( "div", null, {
+        style: { 
+            "text-align": "left"
+        }
+    } );
+    actionsTd.appendChild( div );
+
+    for ( var a in instance.actions ) {
+        instanceButton( div, instance.name, instance.actions[a] );
+    }
+    parent.appendChild( actionsTd );
+}
+
+function instanceButton( /* Element */ parent, /* String */ name, /* Obj */ action ) {
+    var input = createElement( "input", null, {
+        type: 'image',
+        style: {
+            "margin-left": "10px"
+        },
+        title: action.title,
+        alt: action.title,
+        src: imgRoot + '/bundle_' + action.image + '.png'
+    } );
+    $(input).click( function() {
+        changeInstanceState( action.op, name )
+    } );
+    parent.appendChild( input );
+}
+
+function changeInstanceState( /* String */ action, /* String */ name) {
+    $.post( pluginRoot, {
+        "action": action,
+        "name": name
+    }, function( data ) {
+        renderData( data );
+    }, "json" );
+}
+
+
diff --git a/karaf/webconsole/pom.xml b/karaf/webconsole/pom.xml
index 24c116f..69bc87c 100644
--- a/karaf/webconsole/pom.xml
+++ b/karaf/webconsole/pom.xml
@@ -38,6 +38,7 @@
     <module>features</module>
     <module>gogo</module>
     <module>branding</module>
+    <module>admin</module>
   </modules>
   
 </project>