FELIX-2162, FELIX-2171: rewrite the OBR webconsole page to be more scalable and display detailed informations about a given bundle

git-svn-id: https://svn.apache.org/repos/asf/felix/trunk@919175 13f79535-47bb-0310-9956-ffa450edef68
diff --git a/webconsole/pom.xml b/webconsole/pom.xml
index 9d6d944..64c97d3 100644
--- a/webconsole/pom.xml
+++ b/webconsole/pom.xml
@@ -92,6 +92,7 @@
                             org.osgi.service.http,
                             org.apache.felix.scr;
                             org.apache.felix.shell;
+                            org.apache.felix.bundlerepository;
                             org.osgi.service.*;resolution:=optional,
                             javax.portlet;resolution:=optional,
                             javax.servlet.*;version=2.4,
@@ -100,9 +101,9 @@
                         <Embed-Dependency>
                             <!-- Import/Export-Package parsing -->
                             org.apache.felix.bundlerepository;
-                                inline=org/apache/felix/bundlerepository/R4*.class|
-                                    org/apache/felix/bundlerepository/Util.class|
-                                    org/apache/felix/bundlerepository/VersionRange.class,
+                                inline=org/apache/felix/bundlerepository/impl/R4*.class|
+                                    org/apache/felix/bundlerepository/impl/Util.class|
+                                    org/apache/felix/bundlerepository/impl/VersionRange.class,
                             
                             <!-- ServiceTracker -->
                             org.osgi.compendium;
@@ -166,9 +167,9 @@
                                         <!-- <_donotcopy>(LICENSE.json|NOTICE.bare)</_donotcopy> -->
                                         <Embed-Dependency>
                                             org.apache.felix.bundlerepository;
-                                                inline=org/apache/felix/bundlerepository/R4*.class|
-                                                    org/apache/felix/bundlerepository/Util.class|
-                                                    org/apache/felix/bundlerepository/VersionRange.class
+                                                inline=org/apache/felix/bundlerepository/impl/R4*.class|
+                                                    org/apache/felix/bundlerepository/impl/Util.class|
+                                                    org/apache/felix/bundlerepository/impl/VersionRange.class
                                         </Embed-Dependency>
                                     </instructions>
                                 </configuration>
@@ -232,19 +233,11 @@
             <scope>provided</scope>
         </dependency>
 
-        <!-- OBR Service API -->
-        <dependency>
-            <groupId>org.apache.felix</groupId>
-            <artifactId>org.osgi.service.obr</artifactId>
-            <version>1.0.2</version>
-            <scope>provided</scope>
-        </dependency>
-
         <!--  Parsing Import/Export-Package headers -->
         <dependency>
             <groupId>org.apache.felix</groupId>
             <artifactId>org.apache.felix.bundlerepository</artifactId>
-            <version>1.0.3</version>
+            <version>1.5.0-SNAPSHOT</version>
             <scope>compile</scope>
             <optional>true</optional>
         </dependency>
diff --git a/webconsole/src/main/java/org/apache/felix/webconsole/internal/core/BaseUpdateInstallHelper.java b/webconsole/src/main/java/org/apache/felix/webconsole/internal/core/BaseUpdateInstallHelper.java
index 8ac8d46..1790843 100644
--- a/webconsole/src/main/java/org/apache/felix/webconsole/internal/core/BaseUpdateInstallHelper.java
+++ b/webconsole/src/main/java/org/apache/felix/webconsole/internal/core/BaseUpdateInstallHelper.java
@@ -71,7 +71,7 @@
      * @throws BundleException
      * @throws IOException
      */
-    protected Bundle doRun() throws BundleException, IOException
+    protected Bundle doRun() throws Exception
     {
         // now deploy the resolved bundles
         InputStream bundleStream = null;
@@ -121,14 +121,10 @@
                     { bundle } );
             }
         }
-        catch ( IOException ioe )
+        catch ( Exception ioe )
         {
             getLog().log( LogService.LOG_ERROR, "Cannot install or update bundle from " + bundleFile, ioe );
         }
-        catch ( BundleException be )
-        {
-            getLog().log( LogService.LOG_ERROR, "Cannot install or update bundle from " + bundleFile, be );
-        }
         finally
         {
             if ( bundleFile != null )
diff --git a/webconsole/src/main/java/org/apache/felix/webconsole/internal/core/BundlesServlet.java b/webconsole/src/main/java/org/apache/felix/webconsole/internal/core/BundlesServlet.java
index 45c4ca0..1b6b8a6 100644
--- a/webconsole/src/main/java/org/apache/felix/webconsole/internal/core/BundlesServlet.java
+++ b/webconsole/src/main/java/org/apache/felix/webconsole/internal/core/BundlesServlet.java
@@ -40,10 +40,10 @@
 import javax.servlet.http.HttpServletRequest;
 import javax.servlet.http.HttpServletResponse;
 
-import org.apache.felix.bundlerepository.R4Attribute;
-import org.apache.felix.bundlerepository.R4Export;
-import org.apache.felix.bundlerepository.R4Import;
-import org.apache.felix.bundlerepository.R4Package;
+import org.apache.felix.bundlerepository.impl.R4Attribute;
+import org.apache.felix.bundlerepository.impl.R4Export;
+import org.apache.felix.bundlerepository.impl.R4Import;
+import org.apache.felix.bundlerepository.impl.R4Package;
 import org.apache.felix.webconsole.ConfigurationPrinter;
 import org.apache.felix.webconsole.DefaultVariableResolver;
 import org.apache.felix.webconsole.SimpleWebConsolePlugin;
diff --git a/webconsole/src/main/java/org/apache/felix/webconsole/internal/core/UpdateHelper.java b/webconsole/src/main/java/org/apache/felix/webconsole/internal/core/UpdateHelper.java
index ea63bc7..93ec6b6 100644
--- a/webconsole/src/main/java/org/apache/felix/webconsole/internal/core/UpdateHelper.java
+++ b/webconsole/src/main/java/org/apache/felix/webconsole/internal/core/UpdateHelper.java
@@ -22,23 +22,23 @@
 import java.io.File;
 import java.io.IOException;
 import java.io.InputStream;
+import java.net.URL;
+
+import org.apache.commons.io.IOUtils;
+import org.apache.felix.bundlerepository.RepositoryAdmin;
+import org.apache.felix.bundlerepository.Resolver;
+import org.apache.felix.bundlerepository.Resource;
 import org.apache.felix.webconsole.internal.obr.DeployerThread;
 import org.osgi.framework.Bundle;
 import org.osgi.framework.BundleException;
 import org.osgi.framework.Constants;
+import org.osgi.framework.InvalidSyntaxException;
 import org.osgi.service.log.LogService;
-import org.osgi.service.obr.RepositoryAdmin;
-import org.osgi.service.obr.Resolver;
-import org.osgi.service.obr.Resource;
 
 
 abstract class UpdateHelper extends BaseUpdateInstallHelper
 {
 
-    // Define a constant of that name to prevent NoClassDefFoundError in
-    // updateFromOBR trying to load the class with RepositoryAdmin.class
-    private static final String REPOSITORY_ADMIN_NAME = "org.osgi.service.obr.RepositoryAdmin";
-
     private final Bundle bundle;
 
 
@@ -63,7 +63,7 @@
     }
 
 
-    protected Bundle doRun() throws BundleException, IOException
+    protected Bundle doRun() throws Exception
     {
         // update the bundle from the file if defined
         if ( getBundleFile() != null )
@@ -115,9 +115,9 @@
     }
 
 
-    private boolean updateFromOBR()
+    private boolean updateFromOBR() throws InvalidSyntaxException
     {
-        RepositoryAdmin ra = ( RepositoryAdmin ) getService( REPOSITORY_ADMIN_NAME );
+        RepositoryAdmin ra = ( RepositoryAdmin ) getService( RepositoryAdmin.class.getName() );
         if ( ra != null )
         {
             getLog().log( LogService.LOG_DEBUG, "Trying to update from OSGi Bundle Repository" );
@@ -154,7 +154,7 @@
                         .getOptionalResources() );
 
                     // deploy the resolved bundles and ensure they are started
-                    resolver.deploy( true );
+                    resolver.deploy( Resolver.START );
                     getLog().log( LogService.LOG_INFO, "Bundle updated from OSGi Bundle Repository" );
 
                     return true;
@@ -168,7 +168,7 @@
         }
         else
         {
-            getLog().log( LogService.LOG_INFO, "Cannot update from OSGi Bundle Repository: Service not available" );
+            getLog().log( LogService.LOG_DEBUG, "Cannot updated from OSGi Bundle Repository: Service not available" );
         }
 
         // fallback to false, nothing done
diff --git a/webconsole/src/main/java/org/apache/felix/webconsole/internal/obr/BundleRepositoryRender.java b/webconsole/src/main/java/org/apache/felix/webconsole/internal/obr/BundleRepositoryRender.java
index 1410110..d697b66 100644
--- a/webconsole/src/main/java/org/apache/felix/webconsole/internal/obr/BundleRepositoryRender.java
+++ b/webconsole/src/main/java/org/apache/felix/webconsole/internal/obr/BundleRepositoryRender.java
@@ -19,12 +19,18 @@
 package org.apache.felix.webconsole.internal.obr;
 
 import java.io.IOException;
-import java.net.URL;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Enumeration;
+import java.util.List;
 
 import javax.servlet.ServletException;
 import javax.servlet.http.HttpServletRequest;
 import javax.servlet.http.HttpServletResponse;
 
+import org.apache.felix.bundlerepository.Capability;
+import org.apache.felix.bundlerepository.Reason;
+import org.apache.felix.bundlerepository.Requirement;
 import org.apache.felix.webconsole.DefaultVariableResolver;
 import org.apache.felix.webconsole.SimpleWebConsolePlugin;
 import org.apache.felix.webconsole.WebConsoleUtil;
@@ -34,10 +40,11 @@
 import org.json.JSONObject;
 import org.osgi.framework.Bundle;
 import org.osgi.framework.Constants;
-import org.osgi.service.obr.Repository;
-import org.osgi.service.obr.RepositoryAdmin;
-import org.osgi.service.obr.Resolver;
-import org.osgi.service.obr.Resource;
+import org.apache.felix.bundlerepository.Repository;
+import org.apache.felix.bundlerepository.RepositoryAdmin;
+import org.apache.felix.bundlerepository.Resolver;
+import org.apache.felix.bundlerepository.Resource;
+import org.osgi.framework.InvalidSyntaxException;
 
 /**
  * This class provides a plugin for rendering the available OSGi Bundle Repositories
@@ -47,11 +54,11 @@
 {
     private static final String LABEL = "obr";
     private static final String TITLE = "OSGi Repository";
-    private static final String[] CSS = null;
+    private static final String[] CSS = { "/res/ui/obr.css" };
 
     // Define a constant of that name to prevent NoClassDefFoundError in
     // updateFromOBR trying to load the class with RepositoryAdmin.class
-    private static final String REPOSITORY_ADMIN_NAME = "org.osgi.service.obr.RepositoryAdmin";
+    private static final String REPOSITORY_ADMIN_NAME = RepositoryAdmin.class.getName();
 
     // templates
     private final String TEMPLATE;
@@ -72,9 +79,15 @@
      */
     protected void renderContent( HttpServletRequest request, HttpServletResponse response ) throws IOException
     {
+        String query = request.getQueryString();
+        if (query == null || query.length() == 0)
+        {
+            response.sendRedirect(LABEL + "?list=a");
+            return;
+        }
         // prepare variables
         DefaultVariableResolver vars = ((DefaultVariableResolver) WebConsoleUtil.getVariableResolver(request));
-        vars.put("__data__", getData());
+        vars.put("__data__", getData(request));
 
         response.getWriter().print(TEMPLATE);
     }
@@ -97,17 +110,18 @@
         final String action = request.getParameter("action");
         final String deploy = request.getParameter("deploy");
         final String deploystart = request.getParameter("deploystart");
+        final String optional = request.getParameter("optional");
 
         if (action != null)
         {
             doAction(action, request.getParameter("url"), admin);
-            response.getWriter().print(getData());
+            response.getWriter().print(getData(request));
             return;
         }
 
         if (deploy != null || deploystart != null)
         {
-            doDeploy(request.getParameterValues("bundle"), deploystart != null, admin);
+            doDeploy(request.getParameterValues("bundle"), deploystart != null, optional != null, admin);
             doGet(request, response);
             return;
         }
@@ -128,19 +142,122 @@
         }
     }
 
-    private final String getData()
+    private final String getData(HttpServletRequest request)
     {
         final Bundle[] bundles = getBundleContext().getBundles();
         try
         {
-            return toJSON(getRepositoryAdmin(), bundles).toString();
+            RepositoryAdmin admin = getRepositoryAdmin();
+            Resource[] resources = null;
+            boolean details = request.getParameter("details") != null;
+            if (admin != null)
+            {
+                String list = request.getParameter("list");
+                String query = request.getParameter("query");
+                if (list != null)
+                {
+                    String filter;
+                    if ("-".equals(list))
+                    {
+                        StringBuffer sb = new StringBuffer("(!(|");
+                        for (int c = 0; c < 26; c++)
+                        {
+                            sb.append("(presentationname=").append((char) ('a' + c))
+                                    .append("*)(presentationname=")
+                                    .append((char)('A' + c)).append("*)");
+                        }
+                        sb.append("))");
+                        filter = sb.toString();
+                    }
+                    else
+                    {
+                        filter = "(|(presentationname=" + list.toLowerCase() + "*)(presentationname=" + list.toUpperCase() + "*))";
+                    }
+                    resources = admin.discoverResources(filter);
+                }
+                else if (query != null)
+                {
+                    if (query.indexOf('=') > 0)
+                    {
+                        resources = admin.discoverResources(new Requirement[] { parseRequirement(admin, query) });
+                    }
+                    else
+                    {
+                        resources = admin.discoverResources("(|(presentationame=*" + query + "*)(symbolicname=*" + query + "*))");
+                    }
+                }
+                else
+                {
+                    StringBuffer sb = new StringBuffer("(&");
+                    for (Enumeration e = request.getParameterNames(); e.hasMoreElements();)
+                    {
+                        String k = (String) e.nextElement();
+                        String v = request.getParameter(k);
+                        if (v != null && v.length() > 0
+                                && !"details".equals(k) && !"deploy".equals(k)
+                                && !"deploystart".equals(k) && !"bundle".equals(k)
+                                && !"optional".equals(k))
+                        {
+                            sb.append("(").append(k).append("=").append(v).append(")");
+                        }
+                    }
+                    sb.append(")");
+                    resources = admin.discoverResources(sb.toString());
+
+                }
+            }
+            JSONObject json = new JSONObject();
+            json.put("status", admin != null);
+            if (admin != null)
+            {
+                final Repository repositories[] = admin.listRepositories();
+                for (int i = 0; repositories != null && i < repositories.length; i++)
+                {
+                    json.append("repositories", new JSONObject()
+                            .put("lastModified", repositories[i].getLastModified())
+                            .put("name", repositories[i].getName())
+                            .put("url", repositories[i].getURI()));
+                }
+            }
+            for (int i = 0; resources != null && i < resources.length; i++)
+            {
+                json.append("resources", toJSON(resources[i], bundles, details));
+            }
+            return json.toString();
+        }
+        catch (InvalidSyntaxException e)
+        {
+            log("Failed to parse filter.", e);
+            return "";
         }
         catch (JSONException e)
         {
             log("Failed to serialize repository to JSON object.", e);
             return "";
         }
+    }
 
+    private Requirement parseRequirement(RepositoryAdmin admin, String req) throws InvalidSyntaxException {
+        int p = req.indexOf(':');
+        String name;
+        String filter;
+        if (p > 0) {
+            name = req.substring(0, p);
+            filter = req.substring(p + 1);
+        } else {
+            if (req.contains("package")) {
+                name = "package";
+            } else if (req.contains("service")) {
+                name = "service";
+            } else {
+                name = "bundle";
+            }
+            filter = req;
+        }
+        if (!filter.startsWith("(")) {
+            filter = "(" + filter + ")";
+        }
+        return admin.requirement(name, filter);
     }
 
     private final void doAction(String action, String urlParam, RepositoryAdmin admin)
@@ -149,20 +266,20 @@
         Repository[] repos = admin.listRepositories();
         Repository repo = getRepository(repos, urlParam);
 
-        URL url = repo != null ? repo.getURL() : new URL(urlParam);
+        String uri = repo != null ? repo.getURI() : urlParam;
 
         if ("delete".equals(action))
         {
-            if (!admin.removeRepository(url))
+            if (!admin.removeRepository(uri))
             {
-                throw new ServletException("Failed to remove repository with URL " + url);
+                throw new ServletException("Failed to remove repository with URL " + uri);
             }
         }
         else if ("add".equals(action) || "refresh".equals(action))
         {
             try
             {
-                admin.addRepository(url);
+                admin.addRepository(uri);
             }
             catch (IOException e)
             {
@@ -170,47 +287,53 @@
             }
             catch (Exception e)
             {
-                throw new ServletException("Failed to " + action + " repository " + url
+                throw new ServletException("Failed to " + action + " repository " + uri
                     + ": " + e.toString());
             }
 
         }
     }
 
-    private final void doDeploy(String[] bundles, boolean start, RepositoryAdmin repoAdmin)
+    private final void doDeploy(String[] bundles, boolean start, boolean optional, RepositoryAdmin repoAdmin)
     {
-        // check whether we have to do something
-        if (bundles == null || bundles.length == 0)
+        try
         {
-            log("No resources to deploy");
-            return;
-        }
-
-        Resolver resolver = repoAdmin.resolver();
-
-        // prepare the deployment
-        for (int i = 0; i < bundles.length; i++)
-        {
-            String bundle = bundles[i];
-            if (bundle == null || bundle.equals("-"))
+            // check whether we have to do something
+            if (bundles == null || bundles.length == 0)
             {
-                continue;
+                log("No resources to deploy");
+                return;
             }
 
-            String filter = "(id=" + bundle + ")";
-            Resource[] resources = repoAdmin.discoverResources(filter);
-            if (resources != null && resources.length > 0)
-            {
-                resolver.add(resources[0]);
-            }
-        }
+            Resolver resolver = repoAdmin.resolver();
 
-        DeployerThread dt = new DeployerThread(resolver, new Logger(getBundleContext()),
-            start);
-        dt.start();
+            // prepare the deployment
+            for (int i = 0; i < bundles.length; i++)
+            {
+                String bundle = bundles[i];
+                if (bundle == null || bundle.equals("-"))
+                {
+                    continue;
+                }
+
+                String filter = "(id=" + bundle + ")";
+                Resource[] resources = repoAdmin.discoverResources(filter);
+                if (resources != null && resources.length > 0)
+                {
+                    resolver.add(resources[0]);
+                }
+            }
+
+            DeployerThread dt = new DeployerThread(resolver, new Logger(getBundleContext()), start, optional);
+            dt.start();
+        }
+        catch (InvalidSyntaxException e)
+        {
+            throw new IllegalStateException(e); 
+        }
     }
 
-    private static final Repository getRepository(Repository[] repos, String repositoryUrl)
+    private final Repository getRepository(Repository[] repos, String repositoryUrl)
     {
         if (repositoryUrl == null || repositoryUrl.length() == 0)
         {
@@ -219,7 +342,7 @@
 
         for (int i = 0; i < repos.length; i++)
         {
-            if (repositoryUrl.equals(repos[i].getURL().toString()))
+            if (repositoryUrl.equals(repos[i].getURI()))
             {
                 return repos[i];
             }
@@ -228,43 +351,7 @@
         return null;
     }
 
-    private static final JSONObject toJSON(RepositoryAdmin admin, Bundle[] bundles)
-        throws JSONException
-    {
-        JSONObject json = new JSONObject();
-        json.put("status", admin != null);
-
-        if (admin != null)
-        {
-            final Repository repositories[] = admin.listRepositories();
-            for (int i = 0; repositories != null && i < repositories.length; i++)
-            {
-                json.append("repositories", toJSON(repositories[i], bundles));
-            }
-        }
-
-        return json;
-
-    }
-
-    private static final JSONObject toJSON(Repository repo, Bundle[] bundles)
-        throws JSONException
-    {
-        JSONObject json = new JSONObject() //
-        .put("lastModified", repo.getLastModified()) //
-        .put("name", repo.getName()) //
-        .put("url", repo.getURL()); //
-
-        Resource[] resources = repo.getResources();
-        for (int i = 0; resources != null && i < resources.length; i++)
-        {
-            json.append("resources", toJSON(resources[i], bundles));
-        }
-
-        return json;
-    }
-
-    private static final JSONObject toJSON(Resource resource, Bundle[] bundles)
+    private final JSONObject toJSON(Resource resource, Bundle[] bundles, boolean details)
         throws JSONException
     {
         final String symbolicName = resource.getSymbolicName();
@@ -282,15 +369,55 @@
         .put("id", resource.getId()) //
         .put("presentationname", resource.getPresentationName()) //
         .put("symbolicname", symbolicName) //
-        .put("url", resource.getURL()) //
+        .put("url", resource.getURI()) //
         .put("version", version) //
-        .put("url", resource.getURL()) //
         .put("categories", resource.getCategories()) //
         .put("installed", installed);
 
-        // TODO: do we need these ?
-        // resource.getCapabilities()
-        // resource.getRequirements()
+        if (details)
+        {
+            Capability[] caps = resource.getCapabilities();
+            for (int i = 0; caps != null && i < caps.length; i++)
+            {
+                json.append("capabilities", new JSONObject().
+                    put("name", caps[i].getName()).
+                    put("properties", new JSONObject(caps[i].getProperties())));
+            }
+            Requirement[] reqs = resource.getRequirements();
+            for (int i = 0; reqs != null && i < reqs.length; i++)
+            {
+                json.append("requirements", new JSONObject().
+                    put("name", reqs[i].getName()).
+                    put("filter", reqs[i].getFilter()).
+                    put("optional", reqs[i].isOptional()));
+            }
+
+            final RepositoryAdmin admin = getRepositoryAdmin();
+            final List repos = new ArrayList();
+            repos.add(admin.getSystemRepository());
+            repos.addAll(Arrays.asList(admin.listRepositories()));
+            Resolver resolver = admin.resolver((Repository[]) repos.toArray(new Repository[repos.size()]));
+            resolver.add(resource);
+            resolver.resolve();
+            Resource[] required = resolver.getRequiredResources();
+            for (int i = 0; required != null && i < required.length; i++)
+            {
+                json.append("required", toJSON(required[i], bundles, false));
+            }
+            Resource[] optional = resolver.getOptionalResources();
+            for (int i = 0; optional != null && i < optional.length; i++)
+            {
+                json.append("optional", toJSON(optional[i], bundles, false));
+            }
+            Reason[] unsatisfied = resolver.getUnsatisfiedRequirements();
+            for (int i = 0; unsatisfied != null && i < unsatisfied.length; i++)
+            {
+                json.append("unsatisfied", new JSONObject().
+                    put("name", unsatisfied[i].getRequirement().getName()).
+                    put("filter", unsatisfied[i].getRequirement().getFilter()).
+                    put("optional", unsatisfied[i].getRequirement().isOptional()));
+            }
+        }
         return json;
     }
 
diff --git a/webconsole/src/main/java/org/apache/felix/webconsole/internal/obr/DeployerThread.java b/webconsole/src/main/java/org/apache/felix/webconsole/internal/obr/DeployerThread.java
index d2a0315..0e193eb 100644
--- a/webconsole/src/main/java/org/apache/felix/webconsole/internal/obr/DeployerThread.java
+++ b/webconsole/src/main/java/org/apache/felix/webconsole/internal/obr/DeployerThread.java
@@ -19,11 +19,11 @@
 package org.apache.felix.webconsole.internal.obr;
 
 
+import org.apache.felix.bundlerepository.Reason;
+import org.apache.felix.bundlerepository.Resolver;
+import org.apache.felix.bundlerepository.Resource;
 import org.apache.felix.webconsole.internal.Logger;
 import org.osgi.service.log.LogService;
-import org.osgi.service.obr.Requirement;
-import org.osgi.service.obr.Resolver;
-import org.osgi.service.obr.Resource;
 
 
 public class DeployerThread extends Thread
@@ -35,34 +35,40 @@
 
     private final boolean startBundles;
 
+    private final boolean optionalDependencies;
 
-    public DeployerThread( Resolver obrResolver, Logger logger, boolean startBundles )
+
+    public DeployerThread( Resolver obrResolver, Logger logger, boolean startBundles, boolean optionalDependencies )
     {
-        this( obrResolver, logger, startBundles, "OBR Bundle Deployer" );
+        this( obrResolver, logger, startBundles, optionalDependencies, "OBR Bundle Deployer" );
     }
 
 
-    public DeployerThread( Resolver obrResolver, Logger logger, boolean startBundles, String name )
+    public DeployerThread( Resolver obrResolver, Logger logger, boolean startBundles, boolean optionalDependencies, String name )
     {
         super( name );
         this.obrResolver = obrResolver;
         this.logger = logger;
         this.startBundles = startBundles;
+        this.optionalDependencies = optionalDependencies;
     }
 
 
     public void run()
     {
+        int flags = 0;
+        flags += (startBundles ? Resolver.START : 0);
+        flags += (optionalDependencies ? 0 : Resolver.NO_OPTIONAL_RESOURCES);
         try
         {
-            if ( obrResolver.resolve() )
+            if ( obrResolver.resolve( flags ) )
             {
 
                 logResource( logger, "Installing Requested Resources", obrResolver.getAddedResources() );
                 logResource( logger, "Installing Required Resources", obrResolver.getRequiredResources() );
                 logResource( logger, "Installing Optional Resources", obrResolver.getOptionalResources() );
 
-                obrResolver.deploy( startBundles );
+                obrResolver.deploy( flags );
             }
             else
             {
@@ -91,17 +97,17 @@
     }
 
 
-    public static void logRequirements( Logger logger, String message, Requirement[] req )
+    public static void logRequirements( Logger logger, String message, Reason[] reasons )
     {
         logger.log( LogService.LOG_ERROR, message );
-        for ( int i = 0; req != null && i < req.length; i++ )
+        for ( int i = 0; reasons != null && i < reasons.length; i++ )
         {
-            String moreInfo = req[i].getComment();
+            String moreInfo = reasons[i].getRequirement().getComment();
             if ( moreInfo == null )
             {
-                moreInfo = req[i].getFilter().toString();
+                moreInfo = reasons[i].getRequirement().getFilter().toString();
             }
-            logger.log( LogService.LOG_ERROR, "  " + i + ": " + req[i].getName() + " (" + moreInfo + ")" );
+            logger.log( LogService.LOG_ERROR, "  " + i + ": " + reasons[i].getRequirement().getName() + " (" + moreInfo + ")" );
         }
     }
 
diff --git a/webconsole/src/main/resources/res/ui/jquery.uuid.js b/webconsole/src/main/resources/res/ui/jquery.uuid.js
new file mode 100644
index 0000000..f22bf2d
--- /dev/null
+++ b/webconsole/src/main/resources/res/ui/jquery.uuid.js
@@ -0,0 +1,24 @@
+/*
+Usage 1: define the default prefix by using an object with the property prefix as a parameter which contains a string value; {prefix: 'id'}
+Usage 2: call the function jQuery.uuid() with a string parameter p to be used as a prefix to generate a random uuid;
+Usage 3: call the function jQuery.uuid() with no parameters to generate a uuid with the default prefix; defaul prefix: '' (empty string)
+*/
+
+/*
+Generate fragment of random numbers
+*/
+jQuery._uuid_default_prefix = '';
+jQuery._uuidlet = function () {
+	return(((1+Math.random())*0x10000)|0).toString(16).substring(1);
+};
+/*
+Generates random uuid
+*/
+jQuery.uuid = function (p) {
+	if (typeof(p) == 'object' && typeof(p.prefix) == 'string') {
+		jQuery._uuid_default_prefix = p.prefix;
+	} else {
+		p = p || jQuery._uuid_default_prefix || '';
+		return(p+jQuery._uuidlet()+jQuery._uuidlet()+"-"+jQuery._uuidlet()+"-"+jQuery._uuidlet()+"-"+jQuery._uuidlet()+"-"+jQuery._uuidlet()+jQuery._uuidlet()+jQuery._uuidlet());
+	};
+};
\ No newline at end of file
diff --git a/webconsole/src/main/resources/res/ui/obr.css b/webconsole/src/main/resources/res/ui/obr.css
new file mode 100644
index 0000000..a6ef84c
--- /dev/null
+++ b/webconsole/src/main/resources/res/ui/obr.css
@@ -0,0 +1,6 @@
+label {
+    font-family: Verdana, Arial, sans-serif;
+    font-size: 1em;
+    font-style: normal;
+    font-weight: normal;
+}
\ No newline at end of file
diff --git a/webconsole/src/main/resources/res/ui/obr.js b/webconsole/src/main/resources/res/ui/obr.js
index c770d71..3b2c9fc 100644
--- a/webconsole/src/main/resources/res/ui/obr.js
+++ b/webconsole/src/main/resources/res/ui/obr.js
@@ -22,6 +22,29 @@
 var searchField = false;
 var ifStatusOK = false;
 
+//This prototype is provided by the Mozilla foundation and
+//is distributed under the MIT license.
+//http://www.ibiblio.org/pub/Linux/LICENSES/mit.license
+if (!Array.prototype.map)
+{
+  Array.prototype.map = function(fun /*, thisp*/)
+  {
+    var len = this.length;
+    if (typeof fun != "function")
+      throw new TypeError();
+
+    var res = new Array(len);
+    var thisp = arguments[1];
+    for (var i = 0; i < len; i++)
+    {
+      if (i in this)
+        res[i] = fun.call(thisp, this[i], i, this);
+    }
+
+    return res;
+  };
+}
+
 /* displays a date in the user's local timezone */
 function localTm(time) {
 	return (time ? new Date(time) : new Date()).toLocaleString();
@@ -38,43 +61,337 @@
 	}
 }
 
+function showDetails( symbolicname, version ) {
+    window.location.href = window.location.pathname + '?details&symbolicname=' + symbolicname + '&version=' + version;
+}
+
+function showVersions( symbolicname ) {
+	var _id = symbolicname.replace(/\./g, '_');
+    $("#block" + _id).append("<div id='pluginInlineVersions" + _id + "' style='margin-left: 4em'><ul/></div>");
+    $("#img" + _id).each(function() {
+        $(this).
+            removeClass('ui-icon-triangle-1-e').//right
+            addClass('ui-icon-triangle-1-s');//down
+    });
+    $("#entry" + _id).each(function() {
+        $(this).
+            unbind('click').
+            click(function() {hideVersions(symbolicname)}).
+            attr("title", "Hide Versions");
+    });
+    var versions = [];
+    for (var i in obrData.resources ) {
+        if (obrData.resources[i].symbolicname == symbolicname) {
+            versions.push(obrData.resources[i].version);
+        }
+    }
+    versions.sort();
+    for (var i in versions) {
+        var txt = "<li><a href='javascript: showDetails(\"" + symbolicname + "\",\"" + versions[i] + "\")'>" + versions[i] + "</a></li>";
+        $("#pluginInlineVersions" + _id + " > ul").append(txt);
+    }
+}
+
+function hideVersions( symbolicname ) {
+	var _id = symbolicname.replace(/\./g, '_');
+    $("#img" + _id).each(function() {
+        $(this).
+            removeClass('ui-icon-triangle-1-s').//down
+            addClass('ui-icon-triangle-1-e');//right
+    });
+    $("#pluginInlineVersions" + _id).each(function() {
+        $(this).
+            remove();
+    });
+    $("#entry" + _id).each(function() {
+        $(this).
+            unbind('click').
+            click(function() {showVersions(symbolicname)}).
+            attr("title", "Show Versions");
+    });
+}
+
 function renderResource(res) {
-	// aply filtering
-	var match = searchField.val();
-	if (match) {
-		match = new RegExp( match );
-		if ( !match.test(res.presentationname) ) return;
-	}
-	
 	// proceed with resource
 	var _id = res.symbolicname.replace(/\./g, '_');
-	var _tr = resTable.find('#' + _id);
+	var _tr = resTable.find('#row' + _id);
 
 	if (_tr.length == 0) { // not created yet, create it
-		var _select = createElement('select', null, { name : 'bundle' }, [
-			createElement( 'option', null, { value : '-' }, [
-				text( i18n.selectVersion)
-			]),
-			createElement( 'option', null, { value : res.id }, [
-				text( res.version + (res.installed ? ' *' : '') )
-			])
-		]);
-		_tr = tr( null, { 'id' : _id } , [
-			td( null, null, [ _select ] ),
-			td( null, null, [ text(res.presentationname) ] ),
+	    var blockElement = createElement('span', '', {
+            id: 'block' + _id
+	    });
+        var titleElement = createElement('span', '', {
+            id: 'entry' + _id,
+            title: "Show Versions"
+        });
+        var inputElement = createElement('span', 'ui-icon ui-icon-triangle-1-e', {
+            id: 'img' + _id,
+            style: {display: "inline-block"}
+        });
+        blockElement.appendChild(titleElement);
+        titleElement.appendChild(inputElement);
+        titleElement.appendChild(text(" "));
+        titleElement.appendChild(text(res.presentationname ? res.presentationname : res.symbolicname));
+        $(titleElement).click(function() {showVersions(res.symbolicname)});
+
+		_tr = tr( null, { 'id' : 'row' + _id } , [
+			td( null, null, [ blockElement ] ),
 			td( null, null, [ text(res.installed ? res.version : '') ] )
 		]);
 		resTable.append( _tr );
-	} else { // append the additional version
-		_tr.find( 'select' ).append (
-			createElement( 'option', null, { value : res.id }, [
-				text( res.version  + (res.installed ? ' *' : '') )
-			])
-		);
-		if (res.installed) _tr.find( 'td:eq(2)' ).text( res.version );
 	}
 }
 
+function getCapabilitiesByName(res, name) {
+    var caps = [];
+    for (var v in res.capabilities) {
+        if (res.capabilities[v].name == name) {
+            caps.push(res.capabilities[v]);
+        }
+    }
+    return caps;
+}
+
+function getRequirementsByName(res, name) {
+    var caps = [];
+    for (var v in res.requirements) {
+        if (res.requirements[v].name == name) {
+            caps.push(res.requirements[v]);
+        }
+    }
+    return caps;
+}
+
+function createDetailedTable(enclosing, name, headers, rows, callback) {
+    if (rows && rows.length > 0) {
+        var uuid = jQuery.uuid();
+        var title = createElement('span', null, null, [
+                                createElement('span', 'ui-icon ui-icon-triangle-1-e', { id: "img"+uuid, style: {display: "inline-block"} }),
+                                text(" "),
+                                text(name)
+                             ]);
+        enclosing.append(tr(null, null, [
+            td(null, null, [ title ]),
+            td(null, null, [ createElement('table', 'nicetable ui-widget ui-helper-hidden', { id: "alt1"+uuid }, [
+                                createElement('thead', null, null, [
+                                    tr(null, null, headers.map(function(x) {
+                                        return th('ui-widget-header', null, [text(x)]);
+                                    }))
+                                ]),
+                                createElement('tbody', null, null,
+                                    rows.map(function(x) {
+                                        var values = callback(x);
+                                        var tds = values.map(function(x) {
+                                            return td(null, null, [x]);
+                                        });
+                                        return tr(null, null, tds);
+                                    })
+                                )
+                             ]),
+                             createElement('span', null, { id: "alt2"+uuid }, [
+                                text(rows.length)
+                             ])
+            ])
+        ]));
+        $(title).
+                unbind('click').
+                click(function(event) {
+                    event.preventDefault();
+                    $("#img"+uuid).toggleClass('ui-icon-triangle-1-s').//down
+                                   toggleClass('ui-icon-triangle-1-e');//right
+                    $("#alt1"+uuid).toggle();
+                    $("#alt2"+uuid).toggle();
+                });
+    }
+}
+
+function trim(stringToTrim) {
+	return stringToTrim.replace(/^\s+|\s+$/g,"");
+}
+
+function parseSimpleFilter(filter) {
+    filter = filter.substring(1, filter.length-1);
+    var start = 0;
+    var pos = 0;
+    var c = filter.charAt(pos);
+    while (c != '~' && c != '<' && c != '>' && c != '=' && c != '(' && c != ')') {
+        if (c == '<' && filterChars[pos+1] == '*') {
+            break;
+        }
+        if (c == '*' && filterChars[pos+1] == '>') {
+            break;
+        }
+        pos++;
+        c = filter.charAt(pos);
+    }
+    if (pos == start) {
+        throw ("Missing attr: " + filter.substring(pos));
+    }
+
+    var attr = trim(filter.substring(start, pos));
+    var oper = filter.substring(pos, pos+2);
+    var value;
+    if (oper == '*>' || oper == '~=' || oper == '>=' || oper == '<=' || oper == '<*') {
+        value = trim(filter.substring(pos+2));
+        if (value == '') {
+            throw ("Missing value: " + filter.substring(pos));
+        }
+
+        return { operator: oper, operands: [ attr, value ]};
+    } else {
+        if (c != '=') {
+            throw ("Invalid operator: " + filter.substring(pos));
+        }
+        oper = '=';
+        value = filter.substring(pos+1);
+        if (value == '*' ) {
+            return { operator: '=*', operands: [ attr ]};
+        }
+        return { operator: '=', operands: [ attr, value ]};
+    }
+}
+
+function parseFilter(filter) {
+    if (filter.charAt(0) != "(" || filter.charAt(filter.length-1) != ")") {
+        throw "Wrong parenthesis: " + filter;
+    }
+    if (filter.charAt(1) == "!") {
+        return { operator: filter.charAt(1), operands: [ parseFilter(filter.substring(2, filter.length-1)) ] };
+    }
+    if (filter.charAt(1) == "|" || filter.charAt(1) == "&") {
+        var inner = filter.substring(2, filter.length-1);
+        var flts = inner.match(/\([^\(\)]*(\([^\(\)]*(\([^\(\)]*(\([^\(\)]*\))*[^\(\)]*\))*[^\(\)]*\))*[^\(\)]*\)/g);
+        return { operator: filter.charAt(1), operands: flts.map(function(x) { return parseFilter(x); }) };
+    }
+    return parseSimpleFilter(filter);
+}
+
+function simplify(filter) {
+    if (filter.operator == '&' || filter.operator == '|') {
+        filter.operands = filter.operands.map(function(x) { return simplify(x); });
+    } else if (filter.operator == '!') {
+        if (filter.operands[0].operator == '<=') {
+            filter.operator = '>';
+            filter.operands = filter.operands[0].operands;
+        } else if (filter.operands[0].operator == '>=') {
+            filter.operator = '<';
+            filter.operands = filter.operands[0].operands;
+        }
+    }
+    return filter;
+}
+
+function addRow(tbody, key, value) {
+    if (value) {
+        tbody.append( tr(null, null, [
+            td(null, null, [ text(key) ]),
+            td(null, null, [ text(value) ])
+        ]));
+    }
+}
+
+function renderDetailedResource(res) {
+    var tbody = $('#detailsTableBody');
+
+    tbody.append( tr(null, null, [
+        th('ui-widget-header', null, [
+            text("Resource")
+        ]),
+        th('ui-widget-header', null, [
+            createElement('form', 'button-group', { method: "post"}, [
+                createElement('input', null, { type: "hidden", name: "bundle", value: res.id}),
+                createElement('input', 'ui-state-default ui-corner-all', { type: "submit", name: "deploy", value: "Deploy" }, [ text("dummy")]),
+                createElement('input', 'ui-state-default ui-corner-all', { type: "submit", name: "deploystart", value: "Start" }, [ text("dummy")]),
+                text(" "),
+                createElement('input', 'ui-state-default ui-corner-all', { id: "optional", type: "checkbox", name: "optional" }),
+                text(" "),
+                createElement('label', 'ui-widget', { 'for': "optional" }, [ text("optional") ])
+            ])
+        ])
+    ]));
+
+    addRow(tbody, "Name", res.presentationname);
+    addRow(tbody, "Description", res.description);
+    addRow(tbody, "Symbolic name", res.symbolicname);
+    addRow(tbody, "Version", res.version);
+    addRow(tbody, "URI", res.uri);
+    addRow(tbody, "Documentation", res.documentation);
+    addRow(tbody, "Javadoc", res.javadoc);
+    addRow(tbody, "Source", res.source);
+    addRow(tbody, "License", res.license);
+    addRow(tbody, "Copyright", res.copyright);
+    addRow(tbody, "Size", res.size);
+
+    // Exported packages
+    createDetailedTable(tbody, "Exported packages", ["Package", "Version"],
+                        getCapabilitiesByName(res, "package").sort(function(a,b) {
+                            var pa = a.properties['package'], pb = b.properties['package']; return pa == pb ? 0 : pa < pb ? -1 : +1;
+                        }),
+                        function(p) {
+                            return [ text(p.properties['package']), text(p.properties['version']) ];
+                        });
+    // Exported services
+    createDetailedTable(tbody, "Exported services", ["Service"], getCapabilitiesByName(res, "service"), function(p) {
+                            return [ text(p.properties['service']) ];
+                        });
+    // Imported packages
+    createDetailedTable(tbody, "Imported packages", ["Package", "Version", "Optional"], getRequirementsByName(res, "package"), function(p) {
+                            var f = parseFilter(p.filter);
+                            simplify(f);
+                            var n, vmin = "[0.0.0", vmax = "infinity)";
+                            if (f.operator == '&') {
+                                for (var i in f.operands) {
+                                    var fi = f.operands[i];
+                                    if (fi.operands[0] == 'package' && fi.operator == '=') {
+                                        n = fi.operands[1];
+                                    }
+                                    if (fi.operands[0] == 'version') {
+                                        if (fi.operator == '>=') {
+                                            vmin = '[' + fi.operands[1];
+                                        }
+                                        if (fi.operator == '>') {
+                                            vmin = '(' + fi.operands[1];
+                                        }
+                                        if (fi.operator == '<=') {
+                                            vmax = fi.operands[1] + "]";
+                                        }
+                                        if (fi.operator == '<') {
+                                            vmax = fi.operands[1] + ")";
+                                        }
+                                    }
+                                }
+                            }
+                            return [ text(n ? n : p.filter), text(vmin + ", " + vmax), text(p.optional) ];
+                        });
+    // Imported bundles
+    createDetailedTable(tbody, "Imported bundles", ["Bundle", "Version", "Optional"], getRequirementsByName(res, "bundle"), function(p) {
+                            return [ text(p.filter), text(""), text(p.optional) ];
+                        });
+    // Imported services
+    createDetailedTable(tbody, "Imported bundles", ["Service", "Optional"], getRequirementsByName(res, "service"), function(p) {
+                            return [ text(p.filter), text(p.optional) ];
+                        });
+    // Required dependencies
+    createDetailedTable(tbody, "Dependencies", ["Name", "Version"], res.required, function(p) {
+                            var a = createElement('a', null, { href: (window.location.pathname + '?details&symbolicname=' + p.symbolicname + '&version=' + p.version) });
+                            a.appendChild(text(p.presentationname ? p.presentationname : p.symbolicname));
+                            return [ a, text(p.version) ];
+                        });
+    // Optional dependencies
+    createDetailedTable(tbody, "Optional Dependencies", ["Name", "Version"], res.optional, function(p) {
+                            var a = createElement('a', null, { href: (window.location.pathname + '?details&symbolicname=' + p.symbolicname + '&version=' + p.version) });
+                            a.appendChild(text(p.presentationname ? p.presentationname : p.symbolicname));
+                            return [ a, text(p.version) ];
+                        });
+    // Unsatisfied requirements
+    createDetailedTable(tbody, "Unsatisfied Requirements", ["Requirement", "Optional"], res.unsatisfied, function(p) {
+                            return [ text(p.filter), text(p.optional) ];
+                        });
+
+//    $('#detailsTableBody').append( tr(null, null, [ th('ui-widget-header', { colspan: 2 }, [ text("Resource") ]) ]) );
+//    $('#detailsTableBody').append( tbody );
+}
+
 function renderRepository(repo) {
 	var _tr = repoTableTemplate.clone();
 	_tr.find('td:eq(0)').text( repo.name );
@@ -87,21 +404,27 @@
 		doRepoAction('delete', repo.url);
 	});
 	repoTable.append(_tr);
-	
-	for(var i in repo.resources) {
-		renderResource( repo.resources[i] );
-	}
 }
 
-function renderData(data) {
-	obrData = data;
+function renderData() {
 	repoTable.empty();
 	resTable.empty();
-	if ( data.status ) {
+	if ( obrData.status ) {
 		$('.statline').html(i18n.status_ok);
 		ifStatusOK.removeClass('ui-helper-hidden');
-		for (var i in data.repositories ) {
-			renderRepository( data.repositories[i] );
+		for (var i in obrData.repositories ) {
+			renderRepository( obrData.repositories[i] );
+		}
+		if ($.getUrlVar('details')) {
+		    $('#resTable').addClass('ui-helper-hidden');
+		    $('#detailsTable').removeClass('ui-helper-hidden');
+		    for (var i in obrData.resources ) {
+			    renderDetailedResource( obrData.resources[i] );
+            }
+		} else {
+		    for (var i in obrData.resources ) {
+			    renderResource( obrData.resources[i] );
+            }
 		}
 	} else {
 		$('.statline').html(i18n.status_no);
@@ -109,6 +432,31 @@
 	}
 }
 
+
+$.extend({
+  getUrlVars: function(){
+    var vars = [], hash;
+    var hashes = window.location.href.slice(window.location.href.indexOf('?') + 1).split('&');
+    for(var i = 0; i < hashes.length; i++)
+    {
+      var j = hashes[i].indexOf('=');
+      if (j > 0) {
+        var k = hashes[i].slice(0, j);
+        var v = hashes[i].slice(j + 1);
+        vars.push(k);
+        vars[k] = v;
+      } else {
+        vars.push(hashes[i]);
+        vars[hashes[i]] = true;
+      }
+    }
+    return vars;
+  },
+  getUrlVar: function(name){
+    return $.getUrlVars()[name];
+  }
+});
+
 $(document).ready( function() {
 	repoTable = $('#repoTable tbody');
 	repoTableTemplate = repoTable.find('tr').clone();
@@ -116,14 +464,24 @@
 	resTable = $('#resTable tbody').empty();
 	searchField = $('#searchField');
 	ifStatusOK = $('#ifStatusOK');
+    searchField.val($.getUrlVar('query'));
 
-	$('#addRepoBtn').click(function() {
+	$('#addRepoBtn').click(function(event) {
+        event.preventDefault();
 		doRepoAction('add', addRepoUri.val());
 	});
-	$('#searchBtn').click(function() {
-		renderData(obrData);
-		return false;
+	$('#searchBtn').click(function(event) {
+        event.preventDefault();
+        window.location.href = window.location.pathname + '?query=' + searchField.val();
+	});
+	searchField.keypress(function(event) {
+        if (event.keyCode == 13) {
+            event.preventDefault();
+            $('#searchBtn').click();
+        }
 	});
 
-	renderData(obrData);
-});
\ No newline at end of file
+	renderData();
+    initStaticWidgets();
+});
+
diff --git a/webconsole/src/main/resources/templates/obr.html b/webconsole/src/main/resources/templates/obr.html
index 4cc686d..9797d5c 100644
--- a/webconsole/src/main/resources/templates/obr.html
+++ b/webconsole/src/main/resources/templates/obr.html
@@ -1,4 +1,5 @@
-<script type="text/javascript" src="res/ui/obr.js"></script>
+<script type="text/javascript" src="res/ui/jquery.uuid.js"></script>
+<script type="text/javascript" src="res/ui/obr.js"></script>
 <script type="text/javascript">
 var i18n = {
 	status_ok : '${obr.status.ok}',
@@ -44,28 +45,57 @@
 
 <br/>
 
-<form id="installForm" method="post" action="">
-	<div class="ui-widget-header ui-corner-top buttonGroup">
-		<span style="float: left; margin-left: 1em">${obr.res.title}</span>
-		<input type="text" id="searchField" />
-		<button id="searchBtn">${obr.action.search}</button>
-		<input type="submit" name="deploy" value="${obr.action.deploy}" />
-		<input type="submit" name="deploystart" value="${obr.action.deploystart}" />
-	</div>
+<div class="ui-widget-header ui-corner-top buttonGroup">
+    <span style="float: left; margin-left: 1em">${obr.res.title}</span>
+    <span>
+        <a href="obr?list=a">A</a>
+        <a href="obr?list=b">B</a>
+        <a href="obr?list=c">C</a>
+        <a href="obr?list=d">D</a>
+        <a href="obr?list=e">E</a>
+        <a href="obr?list=f">F</a>
+        <a href="obr?list=g">G</a>
+        <a href="obr?list=h">H</a>
+        <a href="obr?list=i">I</a>
+        <a href="obr?list=j">J</a>
+        <a href="obr?list=k">K</a>
+        <a href="obr?list=l">L</a>
+        <a href="obr?list=m">M</a>
+        <a href="obr?list=n">N</a>
+        <a href="obr?list=o">O</a>
+        <a href="obr?list=p">P</a>
+        <a href="obr?list=q">Q</a>
+        <a href="obr?list=r">R</a>
+        <a href="obr?list=s">S</a>
+        <a href="obr?list=t">T</a>
+        <a href="obr?list=u">U</a>
+        <a href="obr?list=v">V</a>
+        <a href="obr?list=w">W</a>
+        <a href="obr?list=x">X</a>
+        <a href="obr?list=y">Y</a>
+        <a href="obr?list=z">Z</a>
+        <a href="obr?list=-">?</a>
+    </span>
+    <input type="text" id="searchField"/>
+    <button id="searchBtn">${obr.action.search}</button>
+</div>
 
-	<table id="resTable" class="nicetable">
-		<thead>
-			<tr>
-				<th class="col_Version">${version}</th>
-				<th class="col_ResName">${obr.res.name}</th>
-				<th class="col_VersionInst">${obr.res.installedVer}</th>
-			</tr>
-		</thead>
-		<tbody>
-			<tr><td colspan="2">dummy</td></tr>
-		</tbody>
-	</table>
-</form>
+<table id="resTable" class="nicetable">
+    <thead>
+        <tr>
+            <th class="col_ResName">${obr.res.name}</th>
+            <th class="col_VersionInst">${obr.res.installedVer}</th>
+        </tr>
+    </thead>
+    <tbody>
+        <tr><td colspan="2">dummy</td></tr>
+    </tbody>
+</table>
+
+<table id="detailsTable" class="nicetable ui-helper-hidden">
+    <tbody id="detailsTableBody">
+    </tbody>
+</table>
 
 <br/>