]> source.dussan.org Git - sonarqube.git/commitdiff
SONAR-6379 fix response to display many updates per plugin 253/head
authorSébastien Lesaint <sebastien.lesaint@sonarsource.com>
Thu, 23 Apr 2015 13:48:48 +0000 (15:48 +0200)
committerSébastien Lesaint <sebastien.lesaint@sonarsource.com>
Fri, 24 Apr 2015 13:58:33 +0000 (15:58 +0200)
instead of currently displaying as many times the same plugin as there is updates for this plugin
update unit test to test the sample response instead of some other json response specific to the unit test

server/sonar-server/src/main/java/org/sonar/server/platform/ServerComponents.java
server/sonar-server/src/main/java/org/sonar/server/plugins/ws/PluginUpdateAggregator.java [new file with mode: 0644]
server/sonar-server/src/main/java/org/sonar/server/plugins/ws/PluginWSCommons.java
server/sonar-server/src/main/java/org/sonar/server/plugins/ws/UpdatesPluginsWsAction.java
server/sonar-server/src/main/resources/org/sonar/server/plugins/ws/example-updates_plugins.json
server/sonar-server/src/test/java/org/sonar/server/plugins/ws/AbstractUpdateCenterBasedPluginsWsActionTest.java
server/sonar-server/src/test/java/org/sonar/server/plugins/ws/AvailablePluginsWsActionTest.java
server/sonar-server/src/test/java/org/sonar/server/plugins/ws/PluginUpdateAggregateBuilderTest.java [new file with mode: 0644]
server/sonar-server/src/test/java/org/sonar/server/plugins/ws/PluginUpdateAggregatorTest.java [new file with mode: 0644]
server/sonar-server/src/test/java/org/sonar/server/plugins/ws/UpdatesPluginsWsActionTest.java

index db7e78f8c358a70e6095c19a1a337556c25c4571..47f16ba95672f06eebb26cd4324bbe19621ee304 100644 (file)
@@ -227,6 +227,7 @@ import org.sonar.server.plugins.ws.CancelAllPluginsWsAction;
 import org.sonar.server.plugins.ws.InstallPluginsWsAction;
 import org.sonar.server.plugins.ws.InstalledPluginsWsAction;
 import org.sonar.server.plugins.ws.PendingPluginsWsAction;
+import org.sonar.server.plugins.ws.PluginUpdateAggregator;
 import org.sonar.server.plugins.ws.PluginWSCommons;
 import org.sonar.server.plugins.ws.PluginsWs;
 import org.sonar.server.plugins.ws.UninstallPluginsWsAction;
@@ -893,6 +894,7 @@ class ServerComponents {
 
     // Plugins WS
     pico.addSingleton(PluginWSCommons.class);
+    pico.addSingleton(PluginUpdateAggregator.class);
     pico.addSingleton(InstalledPluginsWsAction.class);
     pico.addSingleton(AvailablePluginsWsAction.class);
     pico.addSingleton(UpdatesPluginsWsAction.class);
diff --git a/server/sonar-server/src/main/java/org/sonar/server/plugins/ws/PluginUpdateAggregator.java b/server/sonar-server/src/main/java/org/sonar/server/plugins/ws/PluginUpdateAggregator.java
new file mode 100644 (file)
index 0000000..e159549
--- /dev/null
@@ -0,0 +1,119 @@
+/*
+ * SonarQube, open source software quality management tool.
+ * Copyright (C) 2008-2014 SonarSource
+ * mailto:contact AT sonarsource DOT com
+ *
+ * SonarQube is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * SonarQube is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+ */
+package org.sonar.server.plugins.ws;
+
+import com.google.common.annotations.VisibleForTesting;
+import com.google.common.base.Function;
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.Lists;
+import com.google.common.collect.Maps;
+import org.sonar.updatecenter.common.Plugin;
+import org.sonar.updatecenter.common.PluginUpdate;
+
+import javax.annotation.Nonnull;
+import javax.annotation.Nullable;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.List;
+import java.util.Map;
+
+import static com.google.common.base.Preconditions.checkArgument;
+import static com.google.common.base.Preconditions.checkNotNull;
+import static com.google.common.collect.Iterables.transform;
+import static java.lang.String.format;
+
+public class PluginUpdateAggregator {
+
+  public Collection<PluginUpdateAggregate> aggregate(@Nullable Collection<PluginUpdate> pluginUpdates) {
+    if (pluginUpdates == null || pluginUpdates.isEmpty()) {
+      return Collections.emptyList();
+    }
+
+    Map<Plugin, PluginUpdateAggregateBuilder> builders = Maps.newHashMap();
+    for (PluginUpdate pluginUpdate : pluginUpdates) {
+      Plugin plugin = pluginUpdate.getPlugin();
+      PluginUpdateAggregateBuilder builder = builders.get(plugin);
+      if (builder == null) {
+        builder = PluginUpdateAggregateBuilder.builderFor(plugin);
+        builders.put(plugin, builder);
+      }
+      builder.add(pluginUpdate);
+    }
+
+    return Lists.newArrayList(transform(builders.values(), BuilderToPluginUpdateAggregate.INSTANCE));
+  }
+
+  private enum BuilderToPluginUpdateAggregate implements Function<PluginUpdateAggregateBuilder, PluginUpdateAggregate> {
+    INSTANCE;
+
+    @Override
+    public PluginUpdateAggregate apply(@Nonnull PluginUpdateAggregateBuilder input) {
+      return input.build();
+    }
+  }
+
+  @VisibleForTesting
+  static class PluginUpdateAggregateBuilder {
+    private final Plugin plugin;
+
+    private final List<PluginUpdate> updates = Lists.newArrayListWithExpectedSize(1);
+
+    // use static method
+    private PluginUpdateAggregateBuilder(Plugin plugin) {
+      this.plugin = plugin;
+    }
+
+    public static PluginUpdateAggregateBuilder builderFor(Plugin plugin) {
+      return new PluginUpdateAggregateBuilder(checkNotNull(plugin));
+    }
+
+    public PluginUpdateAggregateBuilder add(PluginUpdate pluginUpdate) {
+      checkArgument(
+        this.plugin.equals(pluginUpdate.getPlugin()),
+        format("This builder only accepts PluginUpdate instances for plugin %s", plugin));
+      this.updates.add(pluginUpdate);
+      return this;
+    }
+
+    public PluginUpdateAggregate build() {
+      return new PluginUpdateAggregate(this);
+    }
+  }
+
+  public static class PluginUpdateAggregate {
+    private final Plugin plugin;
+
+    private final Collection<PluginUpdate> updates;
+
+    protected PluginUpdateAggregate(PluginUpdateAggregateBuilder builder) {
+      this.plugin = builder.plugin;
+      this.updates = ImmutableList.copyOf(builder.updates);
+    }
+
+    public Plugin getPlugin() {
+      return plugin;
+    }
+
+    public Collection<PluginUpdate> getUpdates() {
+      return updates;
+    }
+
+  }
+}
index 5924784a2926cd1d5d0e9aca7c1733effc782476..a1367b4e66db6e296838291b14a2c1eb3b1ed655 100644 (file)
@@ -30,7 +30,6 @@ import org.sonar.updatecenter.common.PluginUpdate;
 import org.sonar.updatecenter.common.Release;
 
 import javax.annotation.Nonnull;
-import javax.annotation.Nullable;
 import java.util.Comparator;
 
 import static com.google.common.collect.Iterables.filter;
@@ -60,11 +59,13 @@ public class PluginWSCommons {
   public static final Ordering<PluginMetadata> NAME_KEY_PLUGIN_METADATA_COMPARATOR = Ordering.natural()
     .onResultOf(PluginMetadataToName.INSTANCE)
     .compound(Ordering.natural().onResultOf(PluginMetadataToKey.INSTANCE));
-  public static final Comparator<PluginUpdate> NAME_KEY_PLUGIN_UPDATE_ORDERING = Ordering.from(CASE_INSENSITIVE_ORDER)
-    .onResultOf(PluginUpdateToName.INSTANCE)
-    .compound(
-      Ordering.from(CASE_INSENSITIVE_ORDER).onResultOf(PluginUpdateToKey.INSTANCE)
-    );
+  public static final Comparator<Plugin> NAME_KEY_PLUGIN_ORDERING = Ordering.from(CASE_INSENSITIVE_ORDER)
+      .onResultOf(PluginToName.INSTANCE)
+      .compound(
+          Ordering.from(CASE_INSENSITIVE_ORDER).onResultOf(PluginToKey.INSTANCE)
+      );
+  public static final Comparator<PluginUpdate> NAME_KEY_PLUGIN_UPDATE_ORDERING = Ordering.from(NAME_KEY_PLUGIN_ORDERING)
+    .onResultOf(PluginUpdateToPlugin.INSTANCE);
 
   public void writePluginMetadata(JsonWriter jsonWriter, PluginMetadata pluginMetadata) {
     jsonWriter.beginObject();
@@ -142,9 +143,44 @@ public class PluginWSCommons {
     jsonWriter.endObject();
   }
 
+  /**
+   * Write an "update" object to the specified jsonwriter.
+   * <pre>
+   * "update": {
+   *   "status": "COMPATIBLE",
+   *   "requires": [
+   *     {
+   *       "key": "java",
+   *       "name": "Java",
+   *       "description": "SonarQube rule engine."
+   *     }
+   *   ]
+   * }
+   * </pre>
+   */
   public void writeUpdate(JsonWriter jsonWriter, PluginUpdate pluginUpdate) {
     jsonWriter.name(OBJECT_UPDATE);
     jsonWriter.beginObject();
+
+    writeUpdateProperties(jsonWriter, pluginUpdate);
+
+    jsonWriter.endObject();
+  }
+
+  /**
+   * Write the update properties to the specified jsonwriter.
+   * <pre>
+   * "status": "COMPATIBLE",
+   * "requires": [
+   *   {
+   *     "key": "java",
+   *     "name": "Java",
+   *     "description": "SonarQube rule engine."
+   *   }
+   * ]
+   * </pre>
+   */
+  public void writeUpdateProperties(JsonWriter jsonWriter, PluginUpdate pluginUpdate) {
     jsonWriter.prop(PROPERTY_STATUS, toJSon(pluginUpdate.getStatus()));
 
     jsonWriter.name(ARRAY_REQUIRES);
@@ -158,8 +194,6 @@ public class PluginWSCommons {
       jsonWriter.endObject();
     }
     jsonWriter.endArray();
-
-    jsonWriter.endObject();
   }
 
   @VisibleForTesting
@@ -182,10 +216,7 @@ public class PluginWSCommons {
     INSTANCE;
 
     @Override
-    public Artifact apply(@Nullable Release input) {
-      if (input == null) {
-        return null;
-      }
+    public Artifact apply(@Nonnull Release input) {
       return input.getArtifact();
     }
   }
@@ -208,27 +239,30 @@ public class PluginWSCommons {
     }
   }
 
-  private enum PluginUpdateToKey implements Function<PluginUpdate, String> {
+  private enum PluginUpdateToPlugin implements Function<PluginUpdate, Plugin> {
     INSTANCE;
 
     @Override
-    public String apply(@Nullable PluginUpdate input) {
-      if (input == null) {
-        return null;
-      }
-      return input.getPlugin().getKey();
+    public Plugin apply(@Nonnull PluginUpdate input) {
+      return input.getPlugin();
     }
   }
 
-  private enum PluginUpdateToName implements Function<PluginUpdate, String> {
+  private enum PluginToKey implements Function<Plugin, String> {
     INSTANCE;
 
     @Override
-    public String apply(@Nullable PluginUpdate input) {
-      if (input == null) {
-        return null;
-      }
-      return input.getPlugin().getName();
+    public String apply(@Nonnull Plugin input) {
+      return input.getKey();
+    }
+  }
+
+  private enum PluginToName implements Function<Plugin, String> {
+    INSTANCE;
+
+    @Override
+    public String apply(@Nonnull Plugin input) {
+      return input.getName();
     }
   }
 }
index 22af90dc79f42c1566f16f19270ad88da672b4be..16821425eec8d538d04b7f3fc2b25c803ce555eb 100644 (file)
  */
 package org.sonar.server.plugins.ws;
 
+import com.google.common.base.Function;
 import com.google.common.collect.ImmutableSortedSet;
+import com.google.common.collect.Ordering;
 import com.google.common.io.Resources;
 import org.sonar.api.server.ws.Request;
 import org.sonar.api.server.ws.Response;
 import org.sonar.api.server.ws.WebService;
 import org.sonar.api.utils.text.JsonWriter;
 import org.sonar.server.plugins.UpdateCenterMatrixFactory;
+import org.sonar.server.plugins.ws.PluginUpdateAggregator.PluginUpdateAggregate;
+import org.sonar.updatecenter.common.Plugin;
 import org.sonar.updatecenter.common.PluginUpdate;
 
+import javax.annotation.Nonnull;
 import java.util.Collection;
-
-import static org.sonar.server.plugins.ws.PluginWSCommons.NAME_KEY_PLUGIN_UPDATE_ORDERING;
+import java.util.List;
 
 /**
  * Implementation of the {@code updates} action for the Plugins WebService.
@@ -39,14 +43,27 @@ public class UpdatesPluginsWsAction implements PluginsWsAction {
 
   private static final boolean DO_NOT_FORCE_REFRESH = false;
   private static final String ARRAY_PLUGINS = "plugins";
+  private static final String ARRAY_UPDATES = "updates";
+
+  private static final Ordering<PluginUpdateAggregate> NAME_KEY_PLUGIN_UPGRADE_AGGREGATE_ORDERING =
+    Ordering.from(PluginWSCommons.NAME_KEY_PLUGIN_ORDERING).onResultOf(PluginUpdateAggregateToPlugin.INSTANCE);
+  private static final Ordering<PluginUpdate> PLUGIN_UPDATE_BY_VERSION_ORDERING = Ordering.natural().onResultOf(new Function<PluginUpdate, String>() {
+    @Override
+    public String apply(@Nonnull PluginUpdate input) {
+      return input.getRelease().getVersion().toString();
+    }
+  });
 
   private final UpdateCenterMatrixFactory updateCenterMatrixFactory;
   private final PluginWSCommons pluginWSCommons;
+  private final PluginUpdateAggregator aggregator;
 
   public UpdatesPluginsWsAction(UpdateCenterMatrixFactory updateCenterMatrixFactory,
-                                PluginWSCommons pluginWSCommons) {
+    PluginWSCommons pluginWSCommons,
+    PluginUpdateAggregator aggregator) {
     this.updateCenterMatrixFactory = updateCenterMatrixFactory;
     this.pluginWSCommons = pluginWSCommons;
+    this.aggregator = aggregator;
   }
 
   @Override
@@ -54,7 +71,7 @@ public class UpdatesPluginsWsAction implements PluginsWsAction {
     controller.createAction("updates")
       .setDescription("Lists plugins installed on the SonarQube instance for which at least one newer version is available, sorted by plugin name." +
         "br/>" +
-        "Each newer version is a separate entry in the returned list, with its update/compatibility status." +
+        "Each newer version is listed, ordered from the oldest to the newest, with its own update/compatibility status." +
         "<br/>" +
         "Update status values are: [COMPATIBLE, INCOMPATIBLE, REQUIRES_UPGRADE, DEPS_REQUIRE_UPGRADE]")
       .setSince("5.2")
@@ -75,17 +92,50 @@ public class UpdatesPluginsWsAction implements PluginsWsAction {
   private void writePlugins(JsonWriter jsonWriter) {
     jsonWriter.name(ARRAY_PLUGINS);
     jsonWriter.beginArray();
-    for (PluginUpdate pluginUpdate : retrieveUpdatablePlugins()) {
-      pluginWSCommons.writePluginUpdate(jsonWriter, pluginUpdate);
+    for (PluginUpdateAggregate aggregate : retrieveUpdatablePlugins()) {
+      writePluginUpdateAggregate(jsonWriter, aggregate);
     }
     jsonWriter.endArray();
     jsonWriter.endObject();
   }
 
-  private Collection<PluginUpdate> retrieveUpdatablePlugins() {
+  private void writePluginUpdateAggregate(JsonWriter jsonWriter, PluginUpdateAggregate aggregate) {
+    jsonWriter.beginObject();
+    Plugin plugin = aggregate.getPlugin();
+
+    pluginWSCommons.writeMetadata(jsonWriter, plugin);
+
+    writeUpdates(jsonWriter, aggregate.getUpdates());
+
+    jsonWriter.endObject();
+  }
+
+  private void writeUpdates(JsonWriter jsonWriter, Collection<PluginUpdate> pluginUpdates) {
+    jsonWriter.name(ARRAY_UPDATES).beginArray();
+    for (PluginUpdate pluginUpdate : ImmutableSortedSet.copyOf(PLUGIN_UPDATE_BY_VERSION_ORDERING, pluginUpdates)) {
+      jsonWriter.beginObject();
+      pluginWSCommons.writeRelease(jsonWriter, pluginUpdate.getRelease());
+      pluginWSCommons.writeUpdateProperties(jsonWriter, pluginUpdate);
+      jsonWriter.endObject();
+    }
+    jsonWriter.endArray();
+  }
+
+  private Collection<PluginUpdateAggregate> retrieveUpdatablePlugins() {
+    List<PluginUpdate> pluginUpdates = updateCenterMatrixFactory.getUpdateCenter(DO_NOT_FORCE_REFRESH).findPluginUpdates();
+    // aggregates updates of the same plugin to a single object and sort these objects by plugin name then key
     return ImmutableSortedSet.copyOf(
-      NAME_KEY_PLUGIN_UPDATE_ORDERING,
-      updateCenterMatrixFactory.getUpdateCenter(DO_NOT_FORCE_REFRESH).findPluginUpdates()
+      NAME_KEY_PLUGIN_UPGRADE_AGGREGATE_ORDERING,
+      aggregator.aggregate(pluginUpdates)
       );
   }
+
+  private enum PluginUpdateAggregateToPlugin implements Function<PluginUpdateAggregate, Plugin> {
+    INSTANCE;
+
+    @Override
+    public Plugin apply(@Nonnull PluginUpdateAggregate input) {
+      return input.getPlugin();
+    }
+  }
 }
index 2cfc80ca7ae1f9078f2bf61fc756df137f02857e..17e592ee7c1f3444134ce5bba100856a7ffc98e1 100644 (file)
@@ -9,18 +9,32 @@
       "organizationName": "SonarSource",
       "organizationUrl": "http://www.sonarsource.com",
       "termsAndConditionsUrl": "http://dist.sonarsource.com/SonarSource_Terms_And_Conditions.pdf",
-      "release": {
-        "version": "3.2",
-        "date": "2015-03-10",
-        "artifact": {
-          "name": "sonar-abap-plugin-3.2.jar",
-          "url": "http://dist.sonarsource.com/abap/download/sonar-abap-plugin-3.2.jar"
+      "updates": [
+        {
+          "release": {
+            "version": "3.1",
+            "date": "2014-12-21",
+            "artifact": {
+              "name": "sonar-abap-plugin-3.1.jar",
+              "url": "http://dist.sonarsource.com/abap/download/sonar-abap-plugin-3.1.jar"
+            }
+          },
+          "status": "INCOMPATIBLE",
+          "requires": []
+        },
+        {
+          "release": {
+            "version": "3.2",
+            "date": "2015-03-10",
+            "artifact": {
+              "name": "sonar-abap-plugin-3.2.jar",
+              "url": "http://dist.sonarsource.com/abap/download/sonar-abap-plugin-3.2.jar"
+            }
+          },
+          "status": "COMPATIBLE",
+          "requires": []
         }
-      },
-      "update": {
-        "status": "COMPATIBLE",
-        "requires": []
-      }
+      ]
     },
     {
       "key": "android",
       "license": "GNU LGPL 3",
       "organizationName": "SonarSource and Jerome Van Der Linden, Stephane Nicolas, Florian Roncari, Thomas Bores",
       "organizationUrl": "http://www.sonarsource.com",
-      "release": {
-        "version": "1.0",
-        "date": "2014-03-31",
-        "artifact": {
-          "name": "sonar-android-plugin-1.0.jar",
-          "url": "http://repository.codehaus.org/org/codehaus/sonar-plugins/android/sonar-android-plugin/1.0/sonar-android-plugin-1.0.jar"
+      "updates": [
+        {
+          "release": {
+            "version": "1.0",
+            "date": "2014-03-31",
+            "artifact": {
+              "name": "sonar-android-plugin-1.0.jar",
+              "url": "http://repository.codehaus.org/org/codehaus/sonar-plugins/android/sonar-android-plugin/1.0/sonar-android-plugin-1.0.jar"
+            }
+          },
+          "status": "COMPATIBLE",
+          "requires": [
+            {
+              "key": "java",
+              "name": "Java",
+              "description": "SonarQube rule engine."
+            }
+          ]
         }
-      },
-      "update": {
-        "status": "COMPATIBLE",
-        "requires": [
-          {
-            "key": "java",
-            "name": "Java",
-            "description": "SonarQube rule engine."
-          }
-        ]
-      }
+      ]
     }
   ]
 }
\ No newline at end of file
index 94042b738570eab458f6b718e67e06dbf0c5e67c..c570497b297e60172cabd49afce2f186d30212b4 100644 (file)
@@ -21,7 +21,6 @@ package org.sonar.server.plugins.ws;
 
 import org.junit.Before;
 import org.sonar.api.server.ws.Request;
-import org.sonar.api.utils.DateUtils;
 import org.sonar.server.plugins.UpdateCenterMatrixFactory;
 import org.sonar.server.ws.WsTester;
 import org.sonar.updatecenter.common.Plugin;
@@ -47,19 +46,6 @@ public class AbstractUpdateCenterBasedPluginsWsActionTest {
   protected static final Plugin PLUGIN_1 = new Plugin("p_key_1").setName("p_name_1");
   protected static final Plugin PLUGIN_2 = new Plugin("p_key_2").setName("p_name_2").setDescription("p_desc_2");
 
-  protected static final Plugin FULL_PROPERTIES_PLUGIN = new Plugin("p_key")
-    .setName("p_name")
-    .setCategory("p_category")
-    .setDescription("p_description")
-    .setLicense("p_license")
-    .setOrganization("p_orga_name")
-    .setOrganizationUrl("p_orga_url")
-    .setTermsConditionsUrl("p_t_and_c_url");
-  protected static final Release FULL_PROPERTIES_PLUGIN_RELEASE = release(FULL_PROPERTIES_PLUGIN, "1.12.1")
-    .setDate(DateUtils.parseDate("2015-04-16"))
-    .setDownloadUrl("http://p_file.jar")
-    .addOutgoingDependency(release(PLUGIN_1, "0.3.6"))
-    .addOutgoingDependency(release(PLUGIN_2, "1.0.0"));
 
   protected UpdateCenterMatrixFactory updateCenterFactory = mock(UpdateCenterMatrixFactory.class);
   protected UpdateCenter updateCenter = mock(UpdateCenter.class);
@@ -81,9 +67,9 @@ public class AbstractUpdateCenterBasedPluginsWsActionTest {
 
   protected static PluginUpdate pluginUpdate(String key, String name) {
     return PluginUpdate.createWithStatus(
-        new Release(new Plugin(key).setName(name), Version.create("1.0")),
-        COMPATIBLE
-    );
+      new Release(new Plugin(key).setName(name), Version.create("1.0")),
+      COMPATIBLE
+      );
   }
 
   @Before
index 9cee636646151a9ce4f30d4b39afbab41a5a2f38..27a1f24568676583c030af0ad2ed6488eb5827d2 100644 (file)
@@ -21,11 +21,11 @@ package org.sonar.server.plugins.ws;
 
 import org.junit.Test;
 import org.sonar.api.server.ws.WebService;
+import org.sonar.api.utils.DateUtils;
 import org.sonar.server.ws.WsTester;
 import org.sonar.updatecenter.common.Plugin;
 import org.sonar.updatecenter.common.PluginUpdate;
 import org.sonar.updatecenter.common.Release;
-import org.sonar.updatecenter.common.Version;
 
 import static com.google.common.collect.ImmutableList.of;
 import static org.assertj.core.api.Assertions.assertThat;
@@ -38,6 +38,20 @@ import static org.sonar.updatecenter.common.PluginUpdate.Status.REQUIRE_SONAR_UP
 
 public class AvailablePluginsWsActionTest extends AbstractUpdateCenterBasedPluginsWsActionTest {
 
+  private static final Plugin FULL_PROPERTIES_PLUGIN = new Plugin("p_key")
+      .setName("p_name")
+      .setCategory("p_category")
+      .setDescription("p_description")
+      .setLicense("p_license")
+      .setOrganization("p_orga_name")
+      .setOrganizationUrl("p_orga_url")
+      .setTermsConditionsUrl("p_t_and_c_url");
+  private static final Release FULL_PROPERTIES_PLUGIN_RELEASE = release(FULL_PROPERTIES_PLUGIN, "1.12.1")
+      .setDate(DateUtils.parseDate("2015-04-16"))
+      .setDownloadUrl("http://p_file.jar")
+      .addOutgoingDependency(release(PLUGIN_1, "0.3.6"))
+      .addOutgoingDependency(release(PLUGIN_2, "1.0.0"));
+
   private AvailablePluginsWsAction underTest = new AvailablePluginsWsAction(updateCenterFactory, new PluginWSCommons());
 
   @Test
diff --git a/server/sonar-server/src/test/java/org/sonar/server/plugins/ws/PluginUpdateAggregateBuilderTest.java b/server/sonar-server/src/test/java/org/sonar/server/plugins/ws/PluginUpdateAggregateBuilderTest.java
new file mode 100644 (file)
index 0000000..09c5ee8
--- /dev/null
@@ -0,0 +1,58 @@
+/*
+ * SonarQube, open source software quality management tool.
+ * Copyright (C) 2008-2014 SonarSource
+ * mailto:contact AT sonarsource DOT com
+ *
+ * SonarQube is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * SonarQube is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+ */
+package org.sonar.server.plugins.ws;
+
+import org.junit.Test;
+import org.sonar.server.plugins.ws.PluginUpdateAggregator.PluginUpdateAggregateBuilder;
+import org.sonar.updatecenter.common.Plugin;
+import org.sonar.updatecenter.common.PluginUpdate;
+import org.sonar.updatecenter.common.Release;
+import org.sonar.updatecenter.common.Version;
+
+public class PluginUpdateAggregateBuilderTest {
+
+  private static final Plugin PLUGIN_1 = new Plugin("key1");
+  private static final Plugin PLUGIN_2 = new Plugin("key2");
+  private static final Version SOME_VERSION = Version.create("1.0");
+  private static final PluginUpdate.Status SOME_STATUS = PluginUpdate.Status.COMPATIBLE;
+
+  @Test(expected = NullPointerException.class)
+  public void plugin_can_not_be_null_and_builderFor_enforces_it_with_NPE() throws Exception {
+    PluginUpdateAggregateBuilder.builderFor(null);
+  }
+
+  @Test(expected = IllegalArgumentException.class)
+  public void add_throws_IAE_when_plugin_is_not_equal_to_the_one_of_the_builder() throws Exception {
+    PluginUpdateAggregateBuilder builder = PluginUpdateAggregateBuilder.builderFor(PLUGIN_1);
+
+    builder.add(createPluginUpdate(PLUGIN_2));
+  }
+
+  @Test
+  public void add_uses_equals_which_takes_only_key_into_account() throws Exception {
+    PluginUpdateAggregateBuilder builder = PluginUpdateAggregateBuilder.builderFor(PLUGIN_1);
+
+    builder.add(createPluginUpdate(new Plugin(PLUGIN_1.getKey())));
+  }
+
+  private static PluginUpdate createPluginUpdate(Plugin plugin) {
+    return PluginUpdate.createWithStatus(new Release(plugin, SOME_VERSION), SOME_STATUS);
+  }
+}
\ No newline at end of file
diff --git a/server/sonar-server/src/test/java/org/sonar/server/plugins/ws/PluginUpdateAggregatorTest.java b/server/sonar-server/src/test/java/org/sonar/server/plugins/ws/PluginUpdateAggregatorTest.java
new file mode 100644 (file)
index 0000000..b7e6140
--- /dev/null
@@ -0,0 +1,86 @@
+/*
+ * SonarQube, open source software quality management tool.
+ * Copyright (C) 2008-2014 SonarSource
+ * mailto:contact AT sonarsource DOT com
+ *
+ * SonarQube is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * SonarQube is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+ */
+package org.sonar.server.plugins.ws;
+
+import com.google.common.collect.ImmutableList;
+import org.junit.Test;
+import org.sonar.updatecenter.common.Plugin;
+import org.sonar.updatecenter.common.PluginUpdate;
+import org.sonar.updatecenter.common.Release;
+import org.sonar.updatecenter.common.Version;
+
+import java.util.Collection;
+import java.util.Collections;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+public class PluginUpdateAggregatorTest {
+
+  public static final Version SOME_VERSION = Version.create("1.0");
+  public static final PluginUpdate.Status SOME_STATUS = PluginUpdate.Status.COMPATIBLE;
+  private PluginUpdateAggregator underTest = new PluginUpdateAggregator();
+
+  @Test
+  public void aggregates_returns_an_empty_collection_when_plugin_collection_is_null() throws Exception {
+    assertThat(underTest.aggregate(null)).isEmpty();
+  }
+
+  @Test
+  public void aggregates_returns_an_empty_collection_when_plugin_collection_is_empty() throws Exception {
+    assertThat(underTest.aggregate(Collections.<PluginUpdate>emptyList())).isEmpty();
+  }
+
+  @Test
+  public void aggregates_groups_pluginUpdate_per_plugin_key() throws Exception {
+    Collection<PluginUpdateAggregator.PluginUpdateAggregate> aggregates = underTest.aggregate(ImmutableList.of(
+      createPluginUpdate("key1"),
+      createPluginUpdate("key1"),
+      createPluginUpdate("key0"),
+      createPluginUpdate("key2"),
+      createPluginUpdate("key0")
+      ));
+
+    assertThat(aggregates).hasSize(3);
+    assertThat(aggregates).extracting("plugin.key").containsOnlyOnce("key1", "key0", "key2");
+  }
+
+  @Test
+  public void aggregate_put_pluginUpdates_with_same_plugin_in_the_same_PluginUpdateAggregate() throws Exception {
+    PluginUpdate pluginUpdate1 = createPluginUpdate("key1");
+    PluginUpdate pluginUpdate2 = createPluginUpdate("key1");
+    PluginUpdate pluginUpdate3 = createPluginUpdate("key1");
+    Collection<PluginUpdateAggregator.PluginUpdateAggregate> aggregates = underTest.aggregate(ImmutableList.of(
+      pluginUpdate1,
+      pluginUpdate2,
+      pluginUpdate3
+      ));
+
+    assertThat(aggregates).hasSize(1);
+    Collection<PluginUpdate> releases = aggregates.iterator().next().getUpdates();
+    assertThat(releases).hasSize(3);
+    assertThat(releases).contains(pluginUpdate1);
+    assertThat(releases).contains(pluginUpdate2);
+    assertThat(releases).contains(pluginUpdate3);
+  }
+
+  private PluginUpdate createPluginUpdate(String pluginKey) {
+    return PluginUpdate.createWithStatus(new Release(new Plugin(pluginKey), SOME_VERSION), SOME_STATUS);
+  }
+}
index a5baa19f4a75ccec47465917aec2dcc3cf5c4d42..f5cf5b8ef511128e113553e7c1935967102503d7 100644 (file)
@@ -21,17 +21,51 @@ package org.sonar.server.plugins.ws;
 
 import org.junit.Test;
 import org.sonar.api.server.ws.WebService;
+import org.sonar.api.utils.DateUtils;
 import org.sonar.server.ws.WsTester;
+import org.sonar.updatecenter.common.Plugin;
+import org.sonar.updatecenter.common.Release;
 
 import static com.google.common.collect.ImmutableList.of;
 import static org.assertj.core.api.Assertions.assertThat;
 import static org.mockito.Mockito.when;
 import static org.sonar.test.JsonAssert.assertJson;
 import static org.sonar.updatecenter.common.PluginUpdate.Status.COMPATIBLE;
+import static org.sonar.updatecenter.common.PluginUpdate.Status.INCOMPATIBLE;
 
 public class UpdatesPluginsWsActionTest extends AbstractUpdateCenterBasedPluginsWsActionTest {
-
-  private UpdatesPluginsWsAction underTest = new UpdatesPluginsWsAction(updateCenterFactory, new PluginWSCommons());
+  private static final Plugin JAVA_PLUGIN = new Plugin("java")
+    .setName("Java")
+    .setDescription("SonarQube rule engine.");
+  private static final Plugin ABAP_PLUGIN = new Plugin("abap")
+    .setName("ABAP")
+    .setCategory("Languages")
+    .setDescription("Enable analysis and reporting on ABAP projects")
+    .setLicense("Commercial")
+    .setOrganization("SonarSource")
+    .setOrganizationUrl("http://www.sonarsource.com")
+    .setTermsConditionsUrl("http://dist.sonarsource.com/SonarSource_Terms_And_Conditions.pdf");
+  private static final Release ABAP_31 = release(ABAP_PLUGIN, "3.1")
+    .setDate(DateUtils.parseDate("2014-12-21"))
+    .setDownloadUrl("http://dist.sonarsource.com/abap/download/sonar-abap-plugin-3.1.jar");
+  private static final Release ABAP_32 = release(ABAP_PLUGIN, "3.2")
+    .setDate(DateUtils.parseDate("2015-03-10"))
+    .setDownloadUrl("http://dist.sonarsource.com/abap/download/sonar-abap-plugin-3.2.jar");
+  private static final Plugin ANDROID_PLUGIN = new Plugin("android")
+    .setName("Android")
+    .setCategory("Languages")
+    .setDescription("Import Android Lint reports.")
+    .setLicense("GNU LGPL 3")
+    .setOrganization("SonarSource and Jerome Van Der Linden, Stephane Nicolas, Florian Roncari, Thomas Bores")
+    .setOrganizationUrl("http://www.sonarsource.com");
+  private static final Release ANDROID_10 = release(ANDROID_PLUGIN, "1.0")
+    .setDate(DateUtils.parseDate("2014-03-31"))
+    .setDownloadUrl("http://repository.codehaus.org/org/codehaus/sonar-plugins/android/sonar-android-plugin/1.0/sonar-android-plugin-1.0.jar")
+    .addOutgoingDependency(release(JAVA_PLUGIN, "1.0"));
+
+  private UpdatesPluginsWsAction underTest = new UpdatesPluginsWsAction(updateCenterFactory,
+    new PluginWSCommons(), new PluginUpdateAggregator()
+    );
 
   @Test
   public void action_updatable_is_defined() throws Exception {
@@ -58,14 +92,16 @@ public class UpdatesPluginsWsActionTest extends AbstractUpdateCenterBasedPlugins
   }
 
   @Test
-  public void verify_properties_displayed_in_json_per_plugin() throws Exception {
+  public void verify_response_against_example() throws Exception {
     when(updateCenter.findPluginUpdates()).thenReturn(of(
-      pluginUpdate(FULL_PROPERTIES_PLUGIN_RELEASE, COMPATIBLE)
+      pluginUpdate(ABAP_32, COMPATIBLE),
+      pluginUpdate(ABAP_31, INCOMPATIBLE),
+      pluginUpdate(ANDROID_10, COMPATIBLE)
       ));
 
     underTest.handle(request, response);
 
-    assertJson(response.outputAsString()).isSimilarTo(resource("properties_per_plugin.json"));
+    assertJson(response.outputAsString()).isSimilarTo(getClass().getResource("example-updates_plugins.json"));
   }
 
   @Test
@@ -79,10 +115,12 @@ public class UpdatesPluginsWsActionTest extends AbstractUpdateCenterBasedPlugins
     assertJson(response.outputAsString()).isSimilarTo(
       "{" +
         "  \"plugins\": [" +
-        "    {" +
-        "      \"update\": {" +
-        "        \"status\": \"COMPATIBLE\"" +
-        "      }" +
+        "   {" +
+        "      \"updates\": [" +
+        "        {" +
+        "          \"status\": \"COMPATIBLE\"" +
+        "        }" +
+        "      ]" +
         "    }" +
         "  ]" +
         "}"
@@ -90,10 +128,9 @@ public class UpdatesPluginsWsActionTest extends AbstractUpdateCenterBasedPlugins
   }
 
   @Test
-  public void plugins_are_sorted_by_name_then_key_and_made_unique() throws Exception {
+  public void plugins_are_sorted_by_name_and_made_unique() throws Exception {
     when(updateCenter.findPluginUpdates()).thenReturn(of(
       pluginUpdate("key2", "name2"),
-      pluginUpdate("key1", "name2"),
       pluginUpdate("key2", "name2"),
       pluginUpdate("key0", "name0"),
       pluginUpdate("key1", "name1")
@@ -106,19 +143,15 @@ public class UpdatesPluginsWsActionTest extends AbstractUpdateCenterBasedPlugins
         "  \"plugins\": [" +
         "    {" +
         "      \"key\": \"key0\"," +
-        "      \"name\": \"name0\"," +
-        "    }," +
-        "    {" +
-        "      \"key\": \"key1\"," +
-        "      \"name\": \"name1\"," +
+        "      \"name\": \"name0\"" +
         "    }," +
         "    {" +
         "      \"key\": \"key1\"," +
-        "      \"name\": \"name2\"," +
+        "      \"name\": \"name1\"" +
         "    }," +
         "    {" +
         "      \"key\": \"key2\"," +
-        "      \"name\": \"name2\"," +
+        "      \"name\": \"name2\"" +
         "    }," +
         "  ]" +
         "}"