FELIX-691 : Initial version of a console for the deployment admin.

git-svn-id: https://svn.apache.org/repos/asf/felix/trunk@687299 13f79535-47bb-0310-9956-ffa450edef68
diff --git a/webconsole/pom.xml b/webconsole/pom.xml
index f20dff6..8869960 100644
--- a/webconsole/pom.xml
+++ b/webconsole/pom.xml
@@ -50,7 +50,7 @@
             <plugin>
                 <groupId>org.apache.felix</groupId>
                 <artifactId>maven-scr-plugin</artifactId>
-                <version>1.0.4</version>
+                <version>1.0.6</version>
                 <executions>
                     <execution>
                         <id>generate-scr-scrdescriptor</id>
@@ -63,7 +63,7 @@
             <plugin>
                 <groupId>org.apache.felix</groupId>
                 <artifactId>maven-bundle-plugin</artifactId>
-                <version>1.4.1</version>
+                <version>1.4.3</version>
                 <extensions>true</extensions>
                 <configuration>
                     <instructions>
@@ -136,13 +136,13 @@
         <dependency>
             <groupId>org.apache.felix</groupId>
             <artifactId>org.osgi.core</artifactId>
-            <version>1.0.1</version>
+            <version>1.1.0-SNAPSHOT</version>
             <scope>provided</scope>
         </dependency>
         <dependency>
             <groupId>org.apache.felix</groupId>
             <artifactId>org.osgi.compendium</artifactId>
-            <version>1.0.1</version>
+            <version>1.1.0-SNAPSHOT</version>
             <scope>provided</scope>
             <exclusions>
                 <exclusion>
diff --git a/webconsole/src/main/java/org/apache/felix/webconsole/internal/deppack/DepPackServlet.java b/webconsole/src/main/java/org/apache/felix/webconsole/internal/deppack/DepPackServlet.java
new file mode 100644
index 0000000..71d53f9
--- /dev/null
+++ b/webconsole/src/main/java/org/apache/felix/webconsole/internal/deppack/DepPackServlet.java
@@ -0,0 +1,261 @@
+/*
+ * 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.
+ */
+package org.apache.felix.webconsole.internal.deppack;
+
+
+import java.io.IOException;
+import java.io.PrintWriter;
+import java.util.Map;
+
+import javax.servlet.ServletException;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+
+import org.apache.commons.fileupload.FileItem;
+import org.apache.felix.webconsole.AbstractWebConsolePlugin;
+import org.apache.felix.webconsole.internal.BaseWebConsolePlugin;
+import org.apache.felix.webconsole.internal.Util;
+import org.apache.felix.webconsole.internal.servlet.OsgiManager;
+import org.json.JSONException;
+import org.json.JSONWriter;
+import org.osgi.service.component.ComponentContext;
+import org.osgi.service.deploymentadmin.*;
+
+
+/**
+ * The <code>DepPackServlet</code> TODO
+ * @scr.component metainfo="false"
+ * @scr.service interface="javax.servlet.Servlet"
+ * @scr.property name="felix.webconsole.label" valueRef="LABEL"
+ */
+public class DepPackServlet extends BaseWebConsolePlugin
+{
+
+    public static final String LABEL = "deppack";
+
+    public static final String TITLE = "Deployment Packages";
+
+    private static final String ACTION_DEPLOY = "deploydp";
+
+    private static final String ACTION_UNINSTALL = "uninstalldp";
+
+    private static final String PARAMETER_PCK_FILE = "pckfile";
+
+    public String getLabel()
+    {
+        return LABEL;
+    }
+
+
+    public String getTitle()
+    {
+        return TITLE;
+    }
+
+
+    protected void activate(ComponentContext context) {
+        this.activate(context.getBundleContext());
+    }
+
+    protected void deactivate(ComponentContext context) {
+        this.deactivate();
+    }
+
+    protected void doPost(HttpServletRequest req, HttpServletResponse resp)
+            throws ServletException, IOException
+    {
+        // get the uploaded data
+        final String action = getParameter(req, Util.PARAM_ACTION);
+        if ( ACTION_DEPLOY.equals(action))
+        {
+            Map params = ( Map ) req.getAttribute( AbstractWebConsolePlugin.ATTR_FILEUPLOAD );
+            if ( params != null )
+            {
+                final FileItem pck = getFileItem( params, PARAMETER_PCK_FILE, false );
+                final DeploymentAdmin admin = (DeploymentAdmin) this.getService(DeploymentAdmin.class.getName());
+                if ( admin != null )
+                {
+                    try
+                    {
+                        admin.installDeploymentPackage(pck.getInputStream());
+
+                        final String uri = req.getRequestURI();
+                        resp.sendRedirect( uri );
+                        return;
+                    }
+                    catch (DeploymentException e)
+                    {
+                        throw new ServletException("Unable to deploy package.", e);
+                    }
+                }
+            }
+            throw new ServletException("Upload file or deployment admin missing.");
+        }
+        else if (ACTION_UNINSTALL.equals(action))
+        {
+            final String pckId = req.getPathInfo().substring( req.getPathInfo().lastIndexOf( '/' ) + 1 );
+            if ( pckId != null && pckId.length() > 0 )
+            {
+                final DeploymentAdmin admin = (DeploymentAdmin) this.getService(DeploymentAdmin.class.getName());
+                if ( admin != null )
+                {
+                    try
+                    {
+                        final DeploymentPackage pck = admin.getDeploymentPackage(pckId);
+                        if ( pck != null )
+                        {
+                            pck.uninstall();
+                        }
+                    }
+                    catch (DeploymentException e)
+                    {
+                        throw new ServletException("Unable to undeploy package.", e);
+                    }
+                }
+
+            }
+
+            final PrintWriter pw = resp.getWriter();
+            pw.println("{ \"reload\":true }");
+            return;
+        }
+        throw new ServletException("Unknown action: " + action);
+    }
+
+    private FileItem getFileItem( Map params, String name, boolean isFormField )
+    {
+        FileItem[] items = ( FileItem[] ) params.get( name );
+        if ( items != null )
+        {
+            for ( int i = 0; i < items.length; i++ )
+            {
+                if ( items[i].isFormField() == isFormField )
+                {
+                    return items[i];
+                }
+            }
+        }
+
+        // nothing found, fail
+        return null;
+    }
+
+    protected void renderContent( HttpServletRequest request, HttpServletResponse response ) throws ServletException,
+        IOException
+    {
+
+        PrintWriter pw = response.getWriter();
+
+        String appRoot = ( String ) request.getAttribute( OsgiManager.ATTR_APP_ROOT );
+        pw.println( "<script src='" + appRoot + "/res/ui/packages.js' language='JavaScript'></script>" );
+
+        pw.println("<h1>Deployment Admin</h1>");
+        final DeploymentAdmin admin = (DeploymentAdmin) this.getService(DeploymentAdmin.class.getName());
+        if ( admin == null ) {
+            pw.println("<p><em>Deployment Admin is not installed.</em></p>");
+            return;
+        }
+        final DeploymentPackage[] packages = admin.listDeploymentPackages();
+
+        Util.startScript( pw );
+        pw.println( "var packageListData = " );
+        JSONWriter jw = new JSONWriter( pw );
+        try
+        {
+            jw.object();
+
+            jw.key( "data" );
+
+            jw.array();
+
+            for(int i=0; i<packages.length; i++)
+            {
+                packageInfoJson( jw, packages[i] );
+            }
+
+            jw.endArray();
+
+            jw.endObject();
+
+        }
+        catch ( JSONException je )
+        {
+            throw new IOException( je.toString() );
+        }
+
+        pw.println( ";" );
+        pw.println( "renderPackage( packageListData );" );
+        Util.endScript( pw );
+    }
+
+    private void packageInfoJson( JSONWriter jw, DeploymentPackage pack)
+    throws JSONException
+    {
+        jw.object();
+        jw.key( "id" );
+        jw.value( pack.getName() );
+        jw.key( "name" );
+        jw.value( pack.getName());
+        jw.key( "state" );
+        jw.value( pack.getVersion() );
+
+        jw.key( "actions" );
+        jw.array();
+
+        jw.object();
+        jw.key( "enabled" );
+        jw.value( true );
+        jw.key( "name" );
+        jw.value( "Uninstall" );
+        jw.key( "link" );
+        jw.value( ACTION_UNINSTALL );
+        jw.endObject();
+
+        jw.endArray();
+
+        jw.key( "props" );
+        jw.array();
+        keyVal( jw, "Package Name", pack.getName() );
+        keyVal( jw, "Version", pack.getVersion() );
+
+        final StringBuffer buffer = new StringBuffer();
+        for(int i=0; i<pack.getBundleInfos().length; i++) {
+            buffer.append(pack.getBundleInfos()[i].getSymbolicName() );
+            buffer.append(" - " );
+            buffer.append(pack.getBundleInfos()[i].getVersion() );
+            buffer.append("<br/>");
+        }
+        keyVal(jw, "Bundles", buffer.toString());
+
+        jw.endArray();
+
+        jw.endObject();
+    }
+
+    private void keyVal( JSONWriter jw, String key, Object value ) throws JSONException
+    {
+        if ( key != null && value != null )
+        {
+            jw.object();
+            jw.key( "key" );
+            jw.value( key );
+            jw.key( "value" );
+            jw.value( value );
+            jw.endObject();
+        }
+    }
+}
diff --git a/webconsole/src/main/java/org/apache/felix/webconsole/internal/servlet/OsgiManager.java b/webconsole/src/main/java/org/apache/felix/webconsole/internal/servlet/OsgiManager.java
index f438c03..99983b6 100644
--- a/webconsole/src/main/java/org/apache/felix/webconsole/internal/servlet/OsgiManager.java
+++ b/webconsole/src/main/java/org/apache/felix/webconsole/internal/servlet/OsgiManager.java
@@ -18,46 +18,21 @@
 
 
 import java.io.IOException;
-import java.util.Dictionary;
-import java.util.Enumeration;
-import java.util.HashMap;
-import java.util.Hashtable;
-import java.util.Iterator;
-import java.util.Map;
+import java.util.*;
 
-import javax.servlet.GenericServlet;
-import javax.servlet.Servlet;
-import javax.servlet.ServletException;
-import javax.servlet.ServletRequest;
-import javax.servlet.ServletResponse;
+import javax.servlet.*;
 import javax.servlet.http.HttpServletRequest;
 import javax.servlet.http.HttpServletResponse;
 
-import org.apache.felix.webconsole.AbstractWebConsolePlugin;
-import org.apache.felix.webconsole.Action;
-import org.apache.felix.webconsole.Render;
-import org.apache.felix.webconsole.WebConsoleConstants;
-import org.apache.felix.webconsole.internal.Logger;
-import org.apache.felix.webconsole.internal.OsgiManagerPlugin;
-import org.apache.felix.webconsole.internal.Util;
-import org.apache.felix.webconsole.internal.compendium.ComponentConfigurationPrinter;
-import org.apache.felix.webconsole.internal.compendium.ComponentsServlet;
-import org.apache.felix.webconsole.internal.compendium.ConfigManager;
-import org.apache.felix.webconsole.internal.core.BundlesServlet;
-import org.apache.felix.webconsole.internal.core.InstallAction;
-import org.apache.felix.webconsole.internal.core.SetStartLevelAction;
-import org.apache.felix.webconsole.internal.misc.ConfigurationRender;
-import org.apache.felix.webconsole.internal.misc.LicenseServlet;
-import org.apache.felix.webconsole.internal.misc.ShellServlet;
+import org.apache.felix.webconsole.*;
+import org.apache.felix.webconsole.internal.*;
+import org.apache.felix.webconsole.internal.compendium.*;
+import org.apache.felix.webconsole.internal.core.*;
+import org.apache.felix.webconsole.internal.misc.*;
 import org.apache.felix.webconsole.internal.obr.BundleRepositoryRender;
 import org.apache.felix.webconsole.internal.obr.RefreshRepoAction;
-import org.apache.felix.webconsole.internal.system.GCAction;
-import org.apache.felix.webconsole.internal.system.ShutdownAction;
-import org.apache.felix.webconsole.internal.system.ShutdownRender;
-import org.apache.felix.webconsole.internal.system.VMStatRender;
-import org.osgi.framework.BundleContext;
-import org.osgi.framework.ServiceReference;
-import org.osgi.framework.ServiceRegistration;
+import org.apache.felix.webconsole.internal.system.*;
+import org.osgi.framework.*;
 import org.osgi.service.http.HttpContext;
 import org.osgi.service.http.HttpService;
 import org.osgi.service.log.LogService;
diff --git a/webconsole/src/main/resources/res/ui/bundles.js b/webconsole/src/main/resources/res/ui/bundles.js
index 3a2d57a..bc18bd0 100644
--- a/webconsole/src/main/resources/res/ui/bundles.js
+++ b/webconsole/src/main/resources/res/ui/bundles.js
@@ -40,7 +40,6 @@
     footer( columns );
 }
 
-
 function installForm( /* int */ startLevel )
 {
     document.write( "<form method='post' enctype='multipart/form-data'>" );
diff --git a/webconsole/src/main/resources/res/ui/packages.js b/webconsole/src/main/resources/res/ui/packages.js
new file mode 100644
index 0000000..2a895b7
--- /dev/null
+++ b/webconsole/src/main/resources/res/ui/packages.js
@@ -0,0 +1,352 @@
+/*
+ * 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 renderDataTable( /* Array of Data Objects */ components )
+{
+    // number of actions plus 3 -- id, name and state
+    var columns = components.numActions + 3;
+    
+    header( columns );
+
+    if (components.error)
+    {
+        error( columns, components.error );
+    }
+    else
+    {
+        data ( components.data );
+    }
+
+    footer( columns );
+}
+
+
+function header( /* int */ columns )
+{
+    document.write( "<table class='content' cellpadding='0' cellspacing='0' width='100%'>" );
+
+    document.write( "<tr class='content'>" );
+    document.write( "<td colspan='" + columns + "' class='content'>&nbsp;</th>" );
+    document.write( "</tr>" );
+
+    document.write( "<tr class='content'>" );
+    document.write( "<th class='content'>Name</th>" );
+    document.write( "<th class='content' width='100%'>Details</th>" );
+    document.write( "<th class='content'>Version</th>" );
+    document.write( "<th class='content' colspan='" + (columns - 3) + "'>Actions</th>" );
+    document.write( "</tr>" );
+
+}
+
+
+function error( /* int */ columns, /* String */ message )
+{
+    document.write( "<tr class='content'>" );
+    document.write( "<td class='content'>&nbsp;</td>" );
+    document.write( "<td class='content' colspan='" + (columns - 1) + "'>" + message + "</td>" );
+    document.write( "</tr>" );
+}
+
+
+function data( /* Array of Object */ dataArray )
+{
+    // render components
+    if (dataArray.length == 1)
+    {
+        entry( dataArray[0], true );
+    }
+    else {
+        for ( var idx in dataArray )
+        {
+            entry( dataArray[idx] );
+        }
+    }
+}
+
+
+function footer( /* int */ columns )
+{
+    document.write( "<tr class='content'>" );
+    document.write( "<td colspan='" + columns + "' class='content'>&nbsp;</th>" );
+    document.write( "</tr>" );
+
+    document.write( "</table>" );
+}
+
+
+function entry( /* Object */ dataEntry, /* boolean */ singleEntry )
+{
+    var trElement = tr( null, { id: "entry" + dataEntry.id } );
+    entryInternal( trElement,  dataEntry, singleEntry );
+    document.write( serialize( trElement ) );
+
+    // dataEntry detailed properties
+    trElement = tr( null, { id: "entry" + dataEntry.id + "_details" } );
+    if (dataEntry.props)
+    {
+        getDataEntryDetails( trElement, dataEntry.props );
+    }
+    document.write( serialize( trElement ) );
+}
+
+
+function entryInternal( /* Element */ parent, /* Object */ dataEntry, /* boolean */ singleEntry )
+{
+
+    var id = dataEntry.id;
+    var name = dataEntry.name;
+    var state = dataEntry.state;
+    var icon = singleEntry ? "left" : (dataEntry.props ? "down" : "right");
+    var event = singleEntry ? "history.back()" : "showDataEntryDetails(" + id + ")"; 
+
+    parent.appendChild( td( "content right", null, [ text( id ) ] ) );
+    
+    parent.appendChild( td( "content", null, [
+            createElement( "img", null, {
+                src: appRoot + "/res/imgs/" + icon + ".gif",
+                onClick: event,
+                id: "entry" + id + "_inline"
+            } ),
+            text( "\u00a0" ),
+            createElement( "a", null, {
+                href: pluginRoot + "/" + id
+            }, [ text( name ) ]
+            )]
+        )
+    );
+
+    parent.appendChild( td( "content center", null, [ text( state ) ] ) );
+
+    for ( var aidx in dataEntry.actions )
+    {
+        var action = dataEntry.actions[aidx];
+        parent.appendChild( actionButton( action.enabled, id, action.link, action.name ) );
+    }
+}
+
+
+/* Element */ function actionButton( /* boolean */ enabled, /* long */ id, /* String */ op, /* String */ opLabel )
+{
+    var buttonTd = td( "content", { align: "right" } );
+    if ( op )
+    {
+        var input = createElement( "input", "submit", {
+                type: 'button',
+                value: opLabel,
+                onClick: 'changeDataEntryState("' + id + '", "' + op + '");'
+            });
+        if (!enabled)
+        {
+            input.setAttribute( "disabled", true );
+        }
+        buttonTd.appendChild( input );
+    }
+    else
+    {
+        addText( buttonTd, "\u00a0" );
+    }
+    
+    return buttonTd;
+}
+
+
+function getDataEntryDetails( /* Element */ parent, /* Array of Object */ details )
+{
+    parent.appendChild( addText( td( "content"), "\u00a0" ) );
+    
+    var tdEl = td( "content", { colspan: 4 } );
+    parent.appendChild( tdEl );
+    
+    var tableEl = createElement( "table", null, { border: 0 } );
+    tdEl.appendChild( tableEl );
+    
+    var tbody = createElement( "tbody" );
+    tableEl.appendChild( tbody );
+    for (var idx in details)
+    {
+        var prop = details[idx];
+        
+        
+        var trEl = tr();
+        trEl.appendChild( addText( td( "aligntop", { noWrap: true } ), prop.key ) );
+
+        var proptd = td( "aligntop" );
+        trEl.appendChild( proptd );
+        
+        if (prop.value )
+        {
+            var values = new String( prop.value ).split( "<br />" );
+            for (var i=0; i < values.length; i++)
+            {
+                if (i > 0) { proptd.appendChild( createElement( "br" ) ); }
+                addText( proptd, values[i] );
+            }
+        }
+        else
+        {
+            addText( proptd, "\u00a0" );
+        }
+
+        tbody.appendChild( trEl );
+    }
+ }
+
+ 
+function showDetails(bundleId)
+{
+    var span = document.getElementById('bundle' + bundleId + '_details');
+}
+
+
+function showDataEntryDetails( id )
+{
+    var span = document.getElementById( 'entry' + id + '_details' );
+    if (span)
+    {
+        if (span.firstChild)
+        {
+            clearChildren( span );
+            newLinkValue( id, appRoot + "/res/imgs/right.gif" );
+        }
+        else
+        {
+            sendRequest( 'POST', pluginRoot + '/' + id, displayDataEntryDetails );
+            newLinkValue( id, appRoot + "/res/imgs/down.gif" );
+        }
+    }
+}
+
+
+function newLinkValue( /* long */ id, /* String */ newLinkValue )
+{
+    
+    var link = document.getElementById( "entry" + id + "_inline" );
+    if (link)
+    {
+        link.src = newLinkValue;
+    }
+}
+
+
+function displayDataEntryDetails( obj )
+{
+    var span = document.getElementById('entry' + obj.id + '_details');
+    if (span)
+    {
+        clearChildren( span );
+        getDataEntryDetails( span, obj.props );
+    }
+    
+}
+
+
+function changeDataEntryState(/* long */ id, /* String */ action)
+{
+    var parm = pluginRoot + "/" + id + "?action=" + action;
+    sendRequest('POST', parm, dataEntryStateChanged);
+}
+
+    
+function dataEntryStateChanged(obj)
+{
+    if (obj.reload)
+    {
+        document.location = document.location;
+    }
+    else
+    {
+        var id = obj.id;
+        if (obj.state)
+        {
+            // has status, so draw the line
+            if (obj.props)
+            {
+                var span = document.getElementById('entry' + id + '_details');
+                if (span && span.firstChild)
+                {
+                    clearChildren( span );
+                    getDataEntryDetails( span, obj.props );
+                }
+                else
+                {
+                    obj.props = false;
+                }
+            }
+
+            var span = document.getElementById('entry' + id);
+            if (span)
+            {
+                clearChildren( span );
+                entryInternal( span, obj );
+            }
+        }
+        else
+        {
+            // no status, dataEntry has been removed/uninstalled 
+            var span = document.getElementById('entry' + id);
+            if (span)
+            {
+                span.parentNode.removeChild(span);
+            }
+            var span = document.getElementById('entry' + id + '_details');
+            if (span)
+            {
+                span.parentNode.removeChild(span);
+            }
+        }
+    }    
+}
+
+function renderPackage( /* Array of Data Objects */ bundleData )
+{
+
+    // number of actions plus 3 -- id, name and state
+    var columns = 4;
+    
+    header( columns );
+
+    installForm(  );
+
+    if (bundleData.error)
+    {
+        error( columns, bundleData.error );
+    }
+    else
+    {
+        data ( bundleData.data );
+    }
+
+    installForm(  );
+
+    footer( columns );
+}
+
+function installForm( )
+{
+    document.write( "<form method='post' enctype='multipart/form-data'>" );
+    document.write( "<tr class='content'>" );
+    document.write( "<td class='content'>&nbsp;</td>" );
+    document.write( "<td class='content'>" );
+    document.write( "<input type='hidden' name='action' value='deploydp' />" );
+    document.write( "<input class='input' type='file' name='pckfile' size='50'>" );
+    document.write( "</td>" );
+    document.write( "<td class='content' align='right' colspan='5' noWrap>" );
+    document.write( "<input class='submit' style='width:auto' type='submit' value='Install or Update'>" );
+    document.write( "</td>" );
+    document.write( "</tr>" );
+    document.write( "</form>" );
+}