Display plugin description

Show the plugin description coming directly from the Gerrit API
and calculated at build time.

Change-Id: I6ab6d943351195c777b9b4a1c840f1ed2d9a6660
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
index 400505d..ba1069a 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/manager/gson/SmartGson.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/manager/gson/SmartGson.java
@@ -36,6 +36,10 @@
     return SmartJson.of(gson.fromJson(getReader(url), JsonObject.class));
   }
 
+  public SmartJson of(String jsonText) {
+    return SmartJson.of(gson.fromJson(jsonText, JsonObject.class));
+  }
+
   public <T> T get(String url, Class<T> classOfT) throws IOException {
     try (Reader reader = getReader(url)) {
       return gson.fromJson(reader, classOfT);
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
index bc2ff06..d0efb56 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/manager/gson/SmartJson.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/manager/gson/SmartJson.java
@@ -49,7 +49,7 @@
   }
 
   public Optional<SmartJson> getOptional(String fieldName) {
-    if (jsonElem != null && jsonElem.getAsJsonObject().get(fieldName) != null) {
+    if (jsonElem != null && !jsonElem.isJsonNull() && jsonElem.getAsJsonObject().get(fieldName) != null) {
       return Optional.of(SmartJson
           .of(jsonElem.getAsJsonObject().get(fieldName)));
     }
diff --git a/src/main/java/com/googlesource/gerrit/plugins/manager/repository/CorePluginsDescriptions.java b/src/main/java/com/googlesource/gerrit/plugins/manager/repository/CorePluginsDescriptions.java
new file mode 100644
index 0000000..3474b19
--- /dev/null
+++ b/src/main/java/com/googlesource/gerrit/plugins/manager/repository/CorePluginsDescriptions.java
@@ -0,0 +1,42 @@
+// Copyright (C) 2017 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.util.HashMap;
+import java.util.Optional;
+
+import com.google.inject.Inject;
+import com.google.inject.Singleton;
+
+@Singleton
+public class CorePluginsDescriptions {
+  private final HashMap<String, String> pluginsDescriptions;
+
+  @Inject
+  public CorePluginsDescriptions() {
+    pluginsDescriptions = new HashMap<>();
+    pluginsDescriptions.put("commit-message-length-validator",
+        "Plugin to validate that commit messages conform to length limits");
+    pluginsDescriptions.put("download-commands", "Adds the standard download schemes and commands");
+    pluginsDescriptions.put("hooks", "Old-style fork+exec hooks");
+    pluginsDescriptions.put("replication", "Copies to other servers using the Git protocol");
+    pluginsDescriptions.put("reviewnotes", "Annotates merged commits using notes on refs/notes/review");
+    pluginsDescriptions.put("singleusergroup", "GroupBackend enabling users to be directly added to access rules");
+  }
+
+  public Optional<String> get(String plugin) {
+    return Optional.ofNullable(pluginsDescriptions.get(plugin));
+  }
+}
diff --git a/src/main/java/com/googlesource/gerrit/plugins/manager/repository/CorePluginsRepository.java b/src/main/java/com/googlesource/gerrit/plugins/manager/repository/CorePluginsRepository.java
index 7ba1518..af92981 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/manager/repository/CorePluginsRepository.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/manager/repository/CorePluginsRepository.java
@@ -42,11 +42,15 @@
   private static final Logger log = LoggerFactory
       .getLogger(CorePluginsRepository.class);
   private static final String GERRIT_VERSION = Version.getVersion();
+
   private final SitePaths site;
+  private final CorePluginsDescriptions pluginsDescriptions;
 
   @Inject
-  public CorePluginsRepository(SitePaths site) {
+  public CorePluginsRepository(SitePaths site,
+      CorePluginsDescriptions pd) {
     this.site = site;
+    this.pluginsDescriptions = pd;
   }
 
   static class SelectPluginsFromJar implements Predicate<JarEntry> {
@@ -58,7 +62,7 @@
     }
   }
 
-  static class ExtractPluginInfoFromJarEntry implements
+  class ExtractPluginInfoFromJarEntry implements
       Function<JarEntry, PluginInfo> {
     private String gerritWarFilename;
 
@@ -77,12 +81,14 @@
           Manifest manifestJarEntry = getManifestEntry(pluginJar);
           if (manifestJarEntry != null) {
             Attributes pluginAttributes = manifestJarEntry.getMainAttributes();
+            String pluginName = pluginAttributes.getValue("Gerrit-PluginName");
             return new PluginInfo(
-                pluginAttributes.getValue("Gerrit-PluginName"),
+                pluginName,
+                pluginsDescriptions.get(pluginName).orElse(""),
                 pluginAttributes.getValue("Implementation-Version"), "",
                 pluginUrl.toString());
           }
-          return new PluginInfo(entryName.getFileName().toString(), "", "",
+          return new PluginInfo(entryName.getFileName().toString(), "", "", "",
               pluginUrl.toString());
         } catch (IOException e) {
           log.error("Unable to open plugin " + pluginUrl, e);
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
index 0b609ad..2c5e4db 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/manager/repository/JenkinsCiPluginsRepository.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/manager/repository/JenkinsCiPluginsRepository.java
@@ -43,10 +43,7 @@
 @Singleton
 public class JenkinsCiPluginsRepository implements PluginsRepository {
 
-  private static final Logger log = LoggerFactory
-      .getLogger(JenkinsCiPluginsRepository.class);
-
-  private static final Optional<PluginInfo> noPluginInfo = Optional.empty();
+  private static final Logger log = LoggerFactory.getLogger(JenkinsCiPluginsRepository.class);
 
   private final PluginManagerConfig config;
 
@@ -107,37 +104,27 @@
     return plugins;
   }
 
-  private Optional<PluginInfo> getPluginInfo(final SmartGson gson, String url)
-      throws IOException {
+  private Optional<PluginInfo> getPluginInfo(final SmartGson gson, String url) throws IOException {
     SmartJson jobDetails = gson.get(url + "/api/json");
-    Optional<SmartJson> lastSuccessfulBuild =
-        jobDetails.getOptional("lastSuccessfulBuild");
+    Optional<SmartJson> lastSuccessfulBuild = jobDetails.getOptional("lastSuccessfulBuild");
 
-    return lastSuccessfulBuild.map(
-        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;
-            }
-          }
-        }).orElse(noPluginInfo);
+    return lastSuccessfulBuild.flatMap(new Function<SmartJson, Optional<PluginInfo>>() {
+      @Override
+      public Optional<PluginInfo> apply(SmartJson build) {
+        String buildUrl = build.getString("url");
+        return getPluginArtifactInfo(buildUrl);
+      }
+    });
   }
 
-  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) {
+  private Optional<PluginInfo> getPluginArtifactInfo(String url) {
+    Optional<SmartJson> buildExecution = tryGetJson(url + "/api/json");
+    Optional<JsonArray> artifacts = buildExecution.map(json -> json.get("artifacts").get().getAsJsonArray());
+    if (artifacts.orElse(new JsonArray()).size() == 0) {
       return Optional.empty();
     }
 
-    Optional<SmartJson> artifactJson = findArtifact(artifacts, ".jar");
+    Optional<SmartJson> artifactJson = artifacts.flatMap(a -> findArtifact(a, ".jar"));
     if (!artifactJson.isPresent()) {
       return Optional.empty();
     }
@@ -145,43 +132,64 @@
     String pluginPath = artifactJson.get().getString("relativePath");
 
     String[] pluginPathParts = pluginPath.split("/");
-    String pluginName =
-        isMavenBuild(pluginPathParts)
-            ? fixPluginNameForMavenBuilds(pluginPathParts)
-            : pluginPathParts[pluginPathParts.length - 2];
+    String pluginName = isMavenBuild(pluginPathParts) ? fixPluginNameForMavenBuilds(pluginPathParts)
+        : pluginNameOfJar(pluginPathParts);
 
-    String pluginUrl =
-        String.format("%s/artifact/%s", buildExecution.getString("url"),
-            pluginPath);
+    String pluginUrl = String.format("%s/artifact/%s", buildExecution.get().getString("url"), pluginPath);
 
-    String pluginVersion = "";
-    Optional<SmartJson> verArtifactJson =
-        findArtifact(artifacts, ".jar-version");
-    if (verArtifactJson.isPresent()) {
-      String versionUrl =
-          String.format("%s/artifact/%s", buildExecution.getString("url"),
-              verArtifactJson.get().getString("relativePath"));
-      try (BufferedReader reader =
-          new BufferedReader(new InputStreamReader(
-              new URL(versionUrl).openStream()), 4096)) {
-        pluginVersion = reader.readLine();
-      }
-    }
+    Optional<String> pluginVersion = fetchArtifact(buildExecution.get(), artifacts.get(), ".jar-version");
+    Optional<String> pluginDescription = fetchArtifactJson(buildExecution.get(), artifacts.get(), ".json")
+        .flatMap(json -> json.getOptionalString("description"));
 
-    String sha1 = "";
-    for (JsonElement elem : buildExecution.get("actions").get()
-        .getAsJsonArray()) {
+    for (JsonElement elem : buildExecution.get().get("actions").get().getAsJsonArray()) {
       SmartJson elemJson = SmartJson.of(elem);
-      Optional<SmartJson> lastBuildRevision =
-          elemJson.getOptional("lastBuiltRevision");
+      Optional<SmartJson> lastBuildRevision = elemJson.getOptional("lastBuiltRevision");
 
       if (lastBuildRevision.isPresent()) {
-        sha1 = lastBuildRevision.get().getString("SHA1").substring(0, 8);
+        String sha1 = lastBuildRevision.get().getString("SHA1").substring(0, 8);
+        return pluginVersion
+            .map(version -> new PluginInfo(pluginName, pluginDescription.orElse(""), version, sha1, pluginUrl));
       }
     }
 
-    return Optional.of(new PluginInfo(pluginName, pluginVersion, sha1,
-        pluginUrl));
+    return Optional.empty();
+  }
+
+  private Optional<String> fetchArtifact(SmartJson buildExecution, JsonArray artifacts, String artifactSuffix) {
+    StringBuilder artifactBody = new StringBuilder();
+    Optional<SmartJson> verArtifactJson = findArtifact(artifacts, artifactSuffix);
+    if (verArtifactJson.isPresent()) {
+      String versionUrl = String.format("%s/artifact/%s", buildExecution.getString("url"),
+          verArtifactJson.get().getString("relativePath"));
+      try (BufferedReader reader = new BufferedReader(new InputStreamReader(new URL(versionUrl).openStream()), 4096)) {
+        String line;
+        while ((line = reader.readLine()) != null) {
+          if (artifactBody.length() > 0) {
+            artifactBody.append("\n");
+          }
+          artifactBody.append(line);
+        }
+      } catch(Exception e) {
+        log.error("Unable to fetch artifact from " + versionUrl);
+        return Optional.empty();
+      }
+    }
+    return Optional.of(artifactBody.toString());
+  }
+
+  private Optional<SmartJson> fetchArtifactJson(SmartJson buildExecution, JsonArray artifacts, String artifactSuffix) {
+    Optional<SmartJson> jsonArtifact = findArtifact(artifacts, artifactSuffix);
+    return jsonArtifact.flatMap(artifactJson -> tryGetJson(String.format("%s/artifact/%s",
+        buildExecution.getString("url"), jsonArtifact.get().getString("relativePath"))));
+  }
+
+  private Optional<SmartJson> tryGetJson(String url) {
+    try {
+      return Optional.of(gsonProvider.get().get(url));
+    } catch (IOException e) {
+      log.error("Cannot get JSON from " + url, e);
+      return Optional.empty();
+    }
   }
 
   private String fixPluginNameForMavenBuilds(String[] pluginPathParts) {
@@ -193,6 +201,18 @@
         .substring(0, versionDelim) : mavenPluginFilename;
   }
 
+  private String pluginNameOfJar(String[] pluginJarParts) {
+    int filePos = pluginJarParts.length-1;
+    int pathPos = filePos - 1;
+
+    if(pluginJarParts[filePos].startsWith(pluginJarParts[pathPos])) {
+      return pluginJarParts[pathPos];
+    }
+
+    int jarExtPos = pluginJarParts[filePos].indexOf(".jar");
+    return pluginJarParts[filePos].substring(0, jarExtPos);
+  }
+
   private boolean isMavenBuild(String[] pluginPathParts) {
     return pluginPathParts[pluginPathParts.length - 2].equals("target");
   }
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
index 32bb47f..51a5471 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/manager/repository/PluginInfo.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/manager/repository/PluginInfo.java
@@ -19,13 +19,15 @@
 public class PluginInfo {
   public final String id;
   public final String name;
+  public final String description;
   public final String version;
   public final String sha1;
   public final String url;
 
-  public PluginInfo(String name, String version, String sha1, String url) {
+  public PluginInfo(String name, String description, String version, String sha1, String url) {
     this.id = Url.encode(name);
     this.name = name;
+    this.description = description;
     this.version = version;
     this.sha1 = sha1;
     this.url = url;
diff --git a/src/main/resources/static/index.html b/src/main/resources/static/index.html
index 9ca7d04..a1032aa 100644
--- a/src/main/resources/static/index.html
+++ b/src/main/resources/static/index.html
@@ -39,17 +39,18 @@
           <tr>
             <th>Plugin Name</th>
             <th>Version</th>
-            <th>Upgrade / New Version Available</th>
-            <th>Latest Commit</th>
             <th>Actions</th>
           </tr>
         </thead>
         <tbody>
           <tr ng-repeat="prop in plugins.list | filter:searchPlugin">
-            <td>{{prop.id}}</td>
-            <td>{{prop.version}}</td>
-            <td>{{prop.update_version}}</td>
-            <td>{{prop.sha1}}</td>
+            <td><h4>{{prop.id}}<br/><small>{{prop.description.split('.')[0]}}</small></h4></td>
+            <td>
+              <p>{{prop.version}}</p>
+              <div ng-if="prop.update_version">
+              <span class="glyphicon glyphicon-save"></span><b>&nbsp;{{prop.update_version}}</b>
+              </div>
+            </td>
             <td>
               <h5>
                 <span id="installing-{{prop.id}}"
diff --git a/src/main/resources/static/js/plugin-manager.js b/src/main/resources/static/js/plugin-manager.js
index 5862fe4..5345660 100644
--- a/src/main/resources/static/js/plugin-manager.js
+++ b/src/main/resources/static/js/plugin-manager.js
@@ -47,6 +47,7 @@
                   if (currPluginIdx < 0) {
                     plugins.list.push({
                       id : plugin.id,
+                      description : plugin.description,
                       index_url : plugin.index_url,
                       version : plugin.version,
                       sha1 : '',
@@ -56,6 +57,7 @@
                   } else {
                     plugins.list[currPluginIdx] = {
                       id : plugin.id,
+                      description : plugin.description,
                       index_url : plugin.index_url,
                       version : plugin.version,
                       sha1 : '',
@@ -96,6 +98,7 @@
                       }
                       currPlugin.sha1 = plugin.sha1;
                       currPlugin.url = plugin.url;
+                      currPlugin.description = plugin.description;
 
                       if (currRow < 0) {
                         plugins.list.push(currPlugin);