Adds list of extra plugins available from CI

Display the new plugins available for installation under the
list of the ones currently installed.

Plugins repository is injectable and configurable; at the moment only
the Gerrit CI (gerrit-ci.gerritforge.com) is implemented.

Jenkins CI URL is configured under gerri.config:

[plugin "plugin-manager"]
   jenkinsUrl = https://myjenkins.myserver.com

Change-Id: I7fc5140363700c9edc5c28da460ed8606ed73d78
diff --git a/BUCK b/BUCK
index de8caf8..8499ba4 100644
--- a/BUCK
+++ b/BUCK
@@ -1,3 +1,5 @@
+include_defs('//bucklets/gerrit_plugin.bucklet')
+
 gerrit_plugin(
   name = 'plugin-manager',
   srcs = glob(['src/main/java/**/*.java']),
@@ -9,4 +11,9 @@
     'Implementation-Title: Plugin manager',
     'Implementation-URL: https://gerrit-review.googlesource.com/#/admin/projects/plugins/plugin-manager',
   ],
+  provided_deps = [
+    '//lib:gson',
+    '//lib/log:log4j'
+  ],
 )
+
diff --git a/src/main/java/com/googlesource/gerrit/plugins/manager/AvailablePluginsCollection.java b/src/main/java/com/googlesource/gerrit/plugins/manager/AvailablePluginsCollection.java
new file mode 100644
index 0000000..6c0629c
--- /dev/null
+++ b/src/main/java/com/googlesource/gerrit/plugins/manager/AvailablePluginsCollection.java
@@ -0,0 +1,67 @@
+// Copyright (C) 2015 The Android Open Source Project
+//
+// 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.
+
+package com.googlesource.gerrit.plugins.manager;
+
+import com.google.gerrit.extensions.registration.DynamicMap;
+import com.google.gerrit.extensions.restapi.AcceptsCreate;
+import com.google.gerrit.extensions.restapi.IdString;
+import com.google.gerrit.extensions.restapi.ResourceNotFoundException;
+import com.google.gerrit.extensions.restapi.RestApiException;
+import com.google.gerrit.extensions.restapi.RestCollection;
+import com.google.gerrit.extensions.restapi.RestModifyView;
+import com.google.gerrit.extensions.restapi.RestView;
+import com.google.gerrit.extensions.restapi.TopLevelResource;
+import com.google.gerrit.server.plugins.PluginResource;
+import com.google.inject.Inject;
+import com.google.inject.Provider;
+import com.google.inject.Singleton;
+
+@Singleton
+public class AvailablePluginsCollection implements
+    RestCollection<TopLevelResource, PluginResource>,
+    AcceptsCreate<TopLevelResource> {
+
+  private final DynamicMap<RestView<PluginResource>> views;
+  private final Provider<ListAvailablePlugins> list;
+
+  @Inject
+  AvailablePluginsCollection(DynamicMap<RestView<PluginResource>> views,
+                             Provider<ListAvailablePlugins> list) {
+    this.views = views;
+    this.list = list;
+  }
+
+  @Override
+  public RestView<TopLevelResource> list() throws ResourceNotFoundException {
+    return list.get();
+  }
+
+  @Override
+  public PluginResource parse(TopLevelResource parent, IdString id)
+      throws ResourceNotFoundException {
+      throw new ResourceNotFoundException(id);
+  }
+
+  @Override
+  public DynamicMap<RestView<PluginResource>> views() {
+    return views;
+  }
+
+  @Override
+  public <I> RestModifyView<TopLevelResource, I> create(
+      TopLevelResource parent, IdString id) throws RestApiException {
+    throw new IllegalArgumentException("Operation not supported");
+  }
+}
diff --git a/src/main/java/com/googlesource/gerrit/plugins/manager/GerritVersionBranch.java b/src/main/java/com/googlesource/gerrit/plugins/manager/GerritVersionBranch.java
new file mode 100644
index 0000000..75256b2
--- /dev/null
+++ b/src/main/java/com/googlesource/gerrit/plugins/manager/GerritVersionBranch.java
@@ -0,0 +1,42 @@
+// Copyright (C) 2015 The Android Open Source Project
+//
+// 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.
+
+package com.googlesource.gerrit.plugins.manager;
+
+public class GerritVersionBranch {
+  private static final String GERRIT_NEXT_VERSION = "2.13";
+
+  public static String getBranch(String gerritVersion) {
+    if (gerritVersion == null || gerritVersion.trim().isEmpty()
+        || !Character.isDigit(gerritVersion.trim().charAt(0))
+        || gerritVersion.startsWith(GERRIT_NEXT_VERSION)) {
+      return "master";
+    } else {
+      String[] versionNumbers = gerritVersion.split("\\.");
+
+      if (versionNumbers.length > 2) {
+        String fixVersionNumber = versionNumbers[2];
+        if (fixVersionNumber.contains("-")) {
+          String nextVersion =
+              String.format("%s.%d", versionNumbers[0],
+                  Integer.parseInt(versionNumbers[1]) + 1);
+          if (nextVersion.equals(GERRIT_NEXT_VERSION)) {
+            return "master";
+          }
+        }
+      }
+      return "stable-" + versionNumbers[0] + "." + versionNumbers[1];
+    }
+  }
+}
diff --git a/src/main/java/com/googlesource/gerrit/plugins/manager/ListAvailablePlugins.java b/src/main/java/com/googlesource/gerrit/plugins/manager/ListAvailablePlugins.java
new file mode 100644
index 0000000..d45c16f
--- /dev/null
+++ b/src/main/java/com/googlesource/gerrit/plugins/manager/ListAvailablePlugins.java
@@ -0,0 +1,74 @@
+// Copyright (C) 2015 The Android Open Source Project
+//
+// 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.
+
+package com.googlesource.gerrit.plugins.manager;
+
+import com.google.common.collect.Maps;
+import com.google.gerrit.common.data.GlobalCapability;
+import com.google.gerrit.extensions.annotations.RequiresCapability;
+import com.google.gerrit.extensions.restapi.RestApiException;
+import com.google.gerrit.extensions.restapi.RestReadView;
+import com.google.gerrit.extensions.restapi.TopLevelResource;
+import com.google.gerrit.server.OutputFormat;
+import com.google.gson.JsonElement;
+import com.google.gson.reflect.TypeToken;
+import com.google.inject.Inject;
+
+import com.googlesource.gerrit.plugins.manager.repository.PluginInfo;
+
+import java.io.IOException;
+import java.util.Collections;
+import java.util.Comparator;
+import java.util.List;
+import java.util.Map;
+
+/** List plugins available for installation. */
+@RequiresCapability(GlobalCapability.VIEW_PLUGINS)
+public class ListAvailablePlugins implements RestReadView<TopLevelResource> {
+  private final PluginsCentralLoader loader;
+
+  @Inject
+  public ListAvailablePlugins(PluginsCentralLoader loader) {
+    this.loader = loader;
+  }
+
+  @Override
+  public Object apply(TopLevelResource resource) throws RestApiException {
+    return display();
+  }
+
+  public JsonElement display() throws RestApiException {
+    Map<String, PluginInfo> output = Maps.newTreeMap();
+    List<PluginInfo> plugins;
+    try {
+      plugins = loader.availablePlugins();
+    } catch (IOException e) {
+      throw new RestApiException(
+          "Unable to load the list of available plugins", e);
+    }
+    Collections.sort(plugins, new Comparator<PluginInfo>() {
+      @Override
+      public int compare(PluginInfo a, PluginInfo b) {
+        return a.name.compareTo(b.name);
+      }
+    });
+
+    for (PluginInfo p : plugins) {
+      output.put(p.name, p);
+    }
+
+    return OutputFormat.JSON.newGson().toJsonTree(output,
+        new TypeToken<Map<String, Object>>() {}.getType());
+  }
+}
diff --git a/src/main/java/com/googlesource/gerrit/plugins/manager/PluginManagerRestApiServlet.java b/src/main/java/com/googlesource/gerrit/plugins/manager/PluginManagerRestApiServlet.java
new file mode 100644
index 0000000..bb94616
--- /dev/null
+++ b/src/main/java/com/googlesource/gerrit/plugins/manager/PluginManagerRestApiServlet.java
@@ -0,0 +1,32 @@
+// Copyright (C) 2015 The Android Open Source Project
+//
+// 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.
+
+package com.googlesource.gerrit.plugins.manager;
+
+import com.google.gerrit.httpd.restapi.RestApiServlet;
+import com.google.inject.Inject;
+import com.google.inject.Singleton;
+import com.google.inject.util.Providers;
+
+@Singleton
+public class PluginManagerRestApiServlet extends RestApiServlet {
+  private static final long serialVersionUID = 1L;
+
+  @Inject
+  PluginManagerRestApiServlet(
+      Globals globals,
+      AvailablePluginsCollection members) {
+    super(globals, Providers.of(members));
+  }
+}
diff --git a/src/main/java/com/googlesource/gerrit/plugins/manager/PluginsCentralLoader.java b/src/main/java/com/googlesource/gerrit/plugins/manager/PluginsCentralLoader.java
new file mode 100644
index 0000000..33943af
--- /dev/null
+++ b/src/main/java/com/googlesource/gerrit/plugins/manager/PluginsCentralLoader.java
@@ -0,0 +1,40 @@
+// Copyright (C) 2015 The Android Open Source Project
+//
+// 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.
+
+package com.googlesource.gerrit.plugins.manager;
+
+import com.google.gerrit.common.Version;
+import com.google.inject.Inject;
+import com.google.inject.Singleton;
+
+import com.googlesource.gerrit.plugins.manager.repository.PluginInfo;
+import com.googlesource.gerrit.plugins.manager.repository.PluginsRepository;
+
+import java.io.IOException;
+import java.util.List;
+
+@Singleton
+public class PluginsCentralLoader {
+
+  private final PluginsRepository repository;
+
+  @Inject
+  public PluginsCentralLoader(PluginsRepository repository) {
+    this.repository = repository;
+  }
+
+  public List<PluginInfo> availablePlugins() throws IOException {
+    return repository.list(Version.getVersion());
+  }
+}
diff --git a/src/main/java/com/googlesource/gerrit/plugins/manager/TokenReplaceOutputStream.java b/src/main/java/com/googlesource/gerrit/plugins/manager/TokenReplaceOutputStream.java
index 9810318..2c5976b 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/manager/TokenReplaceOutputStream.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/manager/TokenReplaceOutputStream.java
@@ -30,9 +30,9 @@
 
   private final ByteArrayOutputStream outBuff;
 
-  private HttpServletResponse resp;
+  private final HttpServletResponse resp;
 
-  private int outLen;
+  private final int outLen;
 
   public TokenReplaceOutputStream(HttpServletResponse resp, int contentLength,
       byte[] token, byte[] replace) {
@@ -78,7 +78,6 @@
     ServletOutputStream out = resp.getOutputStream();
     out.write(convertedData.toByteArray());
     out.flush();
-
   }
 
   @Override
diff --git a/src/main/java/com/googlesource/gerrit/plugins/manager/WebModule.java b/src/main/java/com/googlesource/gerrit/plugins/manager/WebModule.java
index 9a32518..c9994f1 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/manager/WebModule.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/manager/WebModule.java
@@ -13,18 +13,20 @@
 // limitations under the License.
 package com.googlesource.gerrit.plugins.manager;
 
-import com.google.gerrit.extensions.restapi.RestApiModule;
 import com.google.inject.servlet.ServletModule;
 
+import com.googlesource.gerrit.plugins.manager.repository.JenkinsCiPluginsRepository;
+import com.googlesource.gerrit.plugins.manager.repository.PluginsRepository;
+
 public class WebModule extends ServletModule {
 
   @Override
   protected void configureServlets() {
-    install(new RestApiModule() {
-      @Override
-      protected void configure() {
-        filterRegex(".*\\.js").through(XAuthFilter.class);
-      }
-    });
+    bind(AvailablePluginsCollection.class);
+    bind(PluginsRepository.class).to(JenkinsCiPluginsRepository.class);
+
+    serve("/available*").with(PluginManagerRestApiServlet.class);
+
+    filterRegex(".*\\.js").through(XAuthFilter.class);
   }
 }
diff --git a/src/main/java/com/googlesource/gerrit/plugins/manager/XAuthFilter.java b/src/main/java/com/googlesource/gerrit/plugins/manager/XAuthFilter.java
index 68874be..33f05b3 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/manager/XAuthFilter.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/manager/XAuthFilter.java
@@ -59,14 +59,14 @@
 
     final String gerritAuth = webSession.get().getXGerritAuth();
     if (gerritAuth != null) {
-      log.info("Injecting X-Gerrit-Auth for {}", httpReq.getRequestURI());
+      log.debug("Injecting X-Gerrit-Auth for {}", httpReq.getRequestURI());
       httpResp = new HttpServletResponseWrapper(httpResp) {
 
         private int origContentLength;
 
         @Override
         public void setHeader(String name, String value) {
-          log.info("{}: {}", name, value);
+          log.debug("{}: {}", name, value);
           if (name.equalsIgnoreCase("Content-Length")) {
             origContentLength = Integer.parseInt(value);
           } else {
diff --git a/src/main/java/com/googlesource/gerrit/plugins/manager/gson/SmartGson.java b/src/main/java/com/googlesource/gerrit/plugins/manager/gson/SmartGson.java
new file mode 100644
index 0000000..400505d
--- /dev/null
+++ b/src/main/java/com/googlesource/gerrit/plugins/manager/gson/SmartGson.java
@@ -0,0 +1,55 @@
+// Copyright (C) 2015 The Android Open Source Project
+//
+// 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.
+
+package com.googlesource.gerrit.plugins.manager.gson;
+
+import com.google.gerrit.server.OutputFormat;
+import com.google.gson.Gson;
+import com.google.gson.JsonObject;
+
+import java.io.IOException;
+import java.io.InputStreamReader;
+import java.io.Reader;
+import java.net.MalformedURLException;
+import java.net.URL;
+
+public class SmartGson {
+
+  private final Gson gson;
+
+  public SmartGson() {
+    this.gson = OutputFormat.JSON.newGson();
+  }
+
+  public SmartJson get(String url) throws IOException {
+    return SmartJson.of(gson.fromJson(getReader(url), JsonObject.class));
+  }
+
+  public <T> T get(String url, Class<T> classOfT) throws IOException {
+    try (Reader reader = getReader(url)) {
+      return gson.fromJson(reader, classOfT);
+    }
+  }
+
+  private InputStreamReader getReader(String url) throws IOException {
+    URL ciUrl;
+    try {
+      ciUrl = new URL(url);
+    } catch (MalformedURLException e) {
+      throw new IllegalArgumentException(
+          "Internal error: Gerrit CI URL seems to be malformed", e);
+    }
+    return new InputStreamReader(ciUrl.openStream());
+  }
+}
diff --git a/src/main/java/com/googlesource/gerrit/plugins/manager/gson/SmartJson.java b/src/main/java/com/googlesource/gerrit/plugins/manager/gson/SmartJson.java
new file mode 100644
index 0000000..7766796
--- /dev/null
+++ b/src/main/java/com/googlesource/gerrit/plugins/manager/gson/SmartJson.java
@@ -0,0 +1,66 @@
+// Copyright (C) 2015 The Android Open Source Project
+//
+// 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.
+
+package com.googlesource.gerrit.plugins.manager.gson;
+
+import com.google.common.base.Function;
+import com.google.common.base.Optional;
+import com.google.gson.JsonElement;
+
+public class SmartJson {
+
+  private final JsonElement jsonElem;
+
+  private SmartJson(JsonElement elem) {
+    this.jsonElem = elem;
+  }
+
+  public static SmartJson of(JsonElement fromJson) {
+    return new SmartJson(fromJson);
+  }
+
+  public Optional<String> getOptionalString(String fieldName) {
+    return getOptional(fieldName).transform(new Function<SmartJson, String>() {
+      @Override
+      public String apply(SmartJson elem) {
+        if (!elem.jsonElem.isJsonPrimitive()) {
+          throw new IllegalArgumentException("cannot convert " + elem.jsonElem
+              + " into a String");
+        }
+        return elem.jsonElem.getAsString();
+      }
+    });
+  }
+
+  public String getString(String fieldName) {
+    return getOptionalString(fieldName).or("");
+  }
+
+  public Optional<SmartJson> getOptional(String fieldName) {
+    if (jsonElem != null && jsonElem.getAsJsonObject().get(fieldName) != null) {
+      return Optional.of(SmartJson
+          .of(jsonElem.getAsJsonObject().get(fieldName)));
+    } else {
+      return Optional.absent();
+    }
+  }
+
+  public SmartJson get(String fieldName) {
+    return getOptional(fieldName).get();
+  }
+
+  public JsonElement get() {
+    return jsonElem;
+  }
+}
diff --git a/src/main/java/com/googlesource/gerrit/plugins/manager/repository/JenkinsCiPluginsRepository.java b/src/main/java/com/googlesource/gerrit/plugins/manager/repository/JenkinsCiPluginsRepository.java
new file mode 100644
index 0000000..d1620b3
--- /dev/null
+++ b/src/main/java/com/googlesource/gerrit/plugins/manager/repository/JenkinsCiPluginsRepository.java
@@ -0,0 +1,150 @@
+// Copyright (C) 2015 The Android Open Source Project
+//
+// 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.
+
+package com.googlesource.gerrit.plugins.manager.repository;
+
+import com.google.common.base.Function;
+import com.google.common.base.Optional;
+import com.google.gerrit.extensions.annotations.PluginName;
+import com.google.gerrit.server.config.PluginConfig;
+import com.google.gerrit.server.config.PluginConfigFactory;
+import com.google.gson.JsonArray;
+import com.google.gson.JsonElement;
+import com.google.inject.Inject;
+import com.google.inject.Provider;
+import com.google.inject.Singleton;
+
+import com.googlesource.gerrit.plugins.manager.GerritVersionBranch;
+import com.googlesource.gerrit.plugins.manager.gson.SmartGson;
+import com.googlesource.gerrit.plugins.manager.gson.SmartJson;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.io.FileNotFoundException;
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.List;
+
+@Singleton
+public class JenkinsCiPluginsRepository implements PluginsRepository {
+
+  private static final Logger log = LoggerFactory
+      .getLogger(JenkinsCiPluginsRepository.class);
+
+  private static final String DEFAULT_GERRIT_CI_URL =
+      "https://gerrit-ci.gerritforge.com";
+
+  private static final Optional<PluginInfo> noPluginInfo = Optional.absent();
+
+  private final PluginConfig config;
+
+  static class View {
+    String name;
+    Job[] jobs;
+  }
+
+  static class Job {
+    String name;
+    String url;
+    String color;
+  }
+
+  private final Provider<SmartGson> gsonProvider;
+
+  @Inject
+  public JenkinsCiPluginsRepository(Provider<SmartGson> gsonProvider,
+      PluginConfigFactory configFactory, @PluginName String pluginName) {
+    this.gsonProvider = gsonProvider;
+    this.config = configFactory.getFromGerritConfig(pluginName);
+  }
+
+  @Override
+  public List<PluginInfo> list(String gerritVersion) throws IOException {
+    SmartGson gson = gsonProvider.get();
+    String viewName = "Plugins-" + GerritVersionBranch.getBranch(gerritVersion);
+    List<PluginInfo> plugins = new ArrayList<>();
+
+    try {
+      Job[] jobs =
+          gson.get(getJenkinsUrl() + "/view/" + viewName + "/api/json",
+              View.class).jobs;
+
+      for (Job job : jobs) {
+        if (job.color.equals("blue")) {
+          Optional<PluginInfo> pluginInfo = getPluginInfo(gson, job.url);
+          if (pluginInfo.isPresent()) {
+            plugins.add(pluginInfo.get());
+          }
+        }
+      }
+    } catch (FileNotFoundException e) {
+      log.warn("No plugins available for Gerrit version " + gerritVersion, e);
+    }
+
+    return plugins;
+  }
+
+  private String getJenkinsUrl() {
+    return config.getString("jenkinsUrl", DEFAULT_GERRIT_CI_URL);
+  }
+
+  private Optional<PluginInfo> getPluginInfo(final SmartGson gson, String url)
+      throws IOException {
+    SmartJson jobDetails = gson.get(url + "/api/json");
+    Optional<SmartJson> lastSuccessfulBuild =
+        jobDetails.getOptional("lastSuccessfulBuild");
+
+    return lastSuccessfulBuild.transform(
+        new Function<SmartJson, Optional<PluginInfo>>() {
+          @Override
+          public Optional<PluginInfo> apply(SmartJson build) {
+            String buildUrl = build.getString("url");
+            try {
+              return getPluginArtifactInfo(gson, buildUrl);
+            } catch (IOException e) {
+              log.warn("Cannot retrieve plugin artifact info from {}", buildUrl);
+              return noPluginInfo;
+            }
+          }
+        }).or(noPluginInfo);
+  }
+
+  private Optional<PluginInfo> getPluginArtifactInfo(SmartGson gson, String url)
+      throws IOException {
+    SmartJson buildExecution = gson.get(url + "/api/json");
+    JsonArray artifacts =
+        buildExecution.get("artifacts").get().getAsJsonArray();
+    if (artifacts.size() == 0) {
+      return Optional.absent();
+    }
+
+    SmartJson artifactJson = SmartJson.of(artifacts.get(0));
+    String pluginFileName = artifactJson.getString("fileName");
+
+    String pluginVersion = "";
+    for (JsonElement elem : buildExecution.get("actions").get()
+        .getAsJsonArray()) {
+      SmartJson elemJson = SmartJson.of(elem);
+      Optional<SmartJson> lastBuildRevision =
+          elemJson.getOptional("lastBuiltRevision");
+
+      if (lastBuildRevision.isPresent()) {
+        pluginVersion = lastBuildRevision.get().getString("SHA1");
+      }
+    }
+
+    return Optional.of(new PluginInfo(pluginFileName, pluginVersion));
+  }
+}
diff --git a/src/main/java/com/googlesource/gerrit/plugins/manager/repository/PluginInfo.java b/src/main/java/com/googlesource/gerrit/plugins/manager/repository/PluginInfo.java
new file mode 100644
index 0000000..03e4d8a
--- /dev/null
+++ b/src/main/java/com/googlesource/gerrit/plugins/manager/repository/PluginInfo.java
@@ -0,0 +1,29 @@
+// Copyright (C) 2015 The Android Open Source Project
+//
+// 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.
+
+package com.googlesource.gerrit.plugins.manager.repository;
+
+import com.google.gerrit.extensions.restapi.Url;
+
+public class PluginInfo {
+  public final String id;
+  public final String name;
+  public final String version;
+
+  PluginInfo(String name, String version) {
+    this.id = Url.encode(name);
+    this.name = name;
+    this.version = version;
+  }
+}
\ No newline at end of file
diff --git a/src/main/java/com/googlesource/gerrit/plugins/manager/repository/PluginsRepository.java b/src/main/java/com/googlesource/gerrit/plugins/manager/repository/PluginsRepository.java
new file mode 100644
index 0000000..236fc28
--- /dev/null
+++ b/src/main/java/com/googlesource/gerrit/plugins/manager/repository/PluginsRepository.java
@@ -0,0 +1,23 @@
+// Copyright (C) 2015 The Android Open Source Project
+//
+// 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.
+
+package com.googlesource.gerrit.plugins.manager.repository;
+
+import java.io.IOException;
+import java.util.List;
+
+public interface PluginsRepository {
+
+  List<PluginInfo> list(String gerritVersion) throws IOException;
+}
diff --git a/src/main/resources/static/index.html b/src/main/resources/static/index.html
index 9d16466..40d710d 100644
--- a/src/main/resources/static/index.html
+++ b/src/main/resources/static/index.html
@@ -11,7 +11,13 @@
     <h1>Installed plugins</h1>
     <ul>
         <li ng-repeat="(key, prop) in plugins.list">
-            {{key}}
+            {{key}} - {{prop.version}}
+        </li>
+    </ul>
+    <h1>Available plugins</h1>
+    <ul>
+        <li ng-repeat="(key, prop) in plugins.available">
+            {{key}} - {{prop.version}}
         </li>
     </ul>
 </div>
diff --git a/src/main/resources/static/js/plugin-manager.js b/src/main/resources/static/js/plugin-manager.js
index ad01b06..d3e455a 100644
--- a/src/main/resources/static/js/plugin-manager.js
+++ b/src/main/resources/static/js/plugin-manager.js
@@ -19,12 +19,18 @@
 
       plugins.list = {};
 
+      plugins.available = {};
+
       $http.get('/plugins/?all', plugins.httpConfig).then(
           function successCallback(response) {
             plugins.list = response.data;
           }, function errorCallback(response) {
-            // called asynchronously if an error occurs
-            // or server returns response with an error status.
+          });
+      
+      $http.get('/plugins/plugin-manager/available', plugins.httpConfig).then(
+          function successCallback(response) {
+            plugins.available = response.data;
+          }, function errorCallback(response) {
           });
     });