]> source.dussan.org Git - sonarqube.git/commitdiff
SONAR-8351 Send JSON payload over HTTP when project analysis is complete
authorSimon Brandhof <simon.brandhof@sonarsource.com>
Tue, 8 Nov 2016 19:43:56 +0000 (20:43 +0100)
committerSimon Brandhof <simon.brandhof@sonarsource.com>
Wed, 9 Nov 2016 20:34:38 +0000 (21:34 +0100)
22 files changed:
server/sonar-ce/src/test/java/org/sonar/ce/container/ComputeEngineContainerImplTest.java
server/sonar-server/src/main/java/org/sonar/server/computation/task/projectanalysis/container/ProjectAnalysisTaskContainerPopulator.java
server/sonar-server/src/main/java/org/sonar/server/computation/task/projectanalysis/webhook/Webhook.java [new file with mode: 0644]
server/sonar-server/src/main/java/org/sonar/server/computation/task/projectanalysis/webhook/WebhookCaller.java [new file with mode: 0644]
server/sonar-server/src/main/java/org/sonar/server/computation/task/projectanalysis/webhook/WebhookCallerImpl.java [new file with mode: 0644]
server/sonar-server/src/main/java/org/sonar/server/computation/task/projectanalysis/webhook/WebhookDelivery.java [new file with mode: 0644]
server/sonar-server/src/main/java/org/sonar/server/computation/task/projectanalysis/webhook/WebhookModule.java [new file with mode: 0644]
server/sonar-server/src/main/java/org/sonar/server/computation/task/projectanalysis/webhook/WebhookPayload.java [new file with mode: 0644]
server/sonar-server/src/main/java/org/sonar/server/computation/task/projectanalysis/webhook/WebhookPostTask.java [new file with mode: 0644]
server/sonar-server/src/main/java/org/sonar/server/computation/task/projectanalysis/webhook/package-info.java [new file with mode: 0644]
server/sonar-server/src/test/java/org/sonar/server/computation/task/projectanalysis/component/TestSettingsRepository.java [new file with mode: 0644]
server/sonar-server/src/test/java/org/sonar/server/computation/task/projectanalysis/webhook/TestWebhookCaller.java [new file with mode: 0644]
server/sonar-server/src/test/java/org/sonar/server/computation/task/projectanalysis/webhook/WebhookCallerImplTest.java [new file with mode: 0644]
server/sonar-server/src/test/java/org/sonar/server/computation/task/projectanalysis/webhook/WebhookPayloadTest.java [new file with mode: 0644]
server/sonar-server/src/test/java/org/sonar/server/computation/task/projectanalysis/webhook/WebhookPostTaskTest.java [new file with mode: 0644]
server/sonar-server/src/test/resources/org/sonar/server/computation/task/projectanalysis/webhook/WebhookPayloadTest/failed.json [new file with mode: 0644]
server/sonar-server/src/test/resources/org/sonar/server/computation/task/projectanalysis/webhook/WebhookPayloadTest/gate_condition_without_value.json [new file with mode: 0644]
server/sonar-server/src/test/resources/org/sonar/server/computation/task/projectanalysis/webhook/WebhookPayloadTest/success.json [new file with mode: 0644]
sonar-core/src/main/java/org/sonar/core/config/CorePropertyDefinitions.java
sonar-core/src/main/java/org/sonar/core/config/WebhookProperties.java [new file with mode: 0644]
sonar-core/src/main/resources/org/sonar/l10n/core.properties
sonar-core/src/test/java/org/sonar/core/config/CorePropertyDefinitionsTest.java

index b9b59c7dc48235b94b1579a35507e7a88c25ae62..b9d1c12bfefc94441dac31b14553edaa3db17ee8 100644 (file)
@@ -108,7 +108,7 @@ public class ComputeEngineContainerImplTest {
         + 25 // level 1
         + 46 // content of DaoModule
         + 2 // content of EsSearchModule
-        + 62 // content of CorePropertyDefinitions
+        + 64 // content of CorePropertyDefinitions
         + 1 // content of CePropertyDefinitions
     );
     assertThat(picoContainer.getParent().getParent().getParent().getParent()).isNull();
index 0431c0983ae1602441a2c43df902e8ce4de2fec7..3c32129608dda1601f2ffa00f046229808819e72 100644 (file)
@@ -101,6 +101,7 @@ import org.sonar.server.computation.task.projectanalysis.source.LastCommitVisito
 import org.sonar.server.computation.task.projectanalysis.source.SourceHashRepositoryImpl;
 import org.sonar.server.computation.task.projectanalysis.source.SourceLinesRepositoryImpl;
 import org.sonar.server.computation.task.projectanalysis.step.ReportComputationSteps;
+import org.sonar.server.computation.task.projectanalysis.webhook.WebhookModule;
 import org.sonar.server.computation.task.step.ComputationStepExecutor;
 import org.sonar.server.computation.task.step.ComputationSteps;
 import org.sonar.server.computation.taskprocessor.MutableTaskResultHolderImpl;
@@ -236,7 +237,10 @@ public final class ProjectAnalysisTaskContainerPopulator implements ContainerPop
       // views
       ViewIndex.class,
 
-      MeasureToMeasureDto.class);
+      MeasureToMeasureDto.class,
+
+      // webhooks
+      WebhookModule.class);
   }
 
 }
diff --git a/server/sonar-server/src/main/java/org/sonar/server/computation/task/projectanalysis/webhook/Webhook.java b/server/sonar-server/src/main/java/org/sonar/server/computation/task/projectanalysis/webhook/Webhook.java
new file mode 100644 (file)
index 0000000..18c1de9
--- /dev/null
@@ -0,0 +1,44 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2016 SonarSource SA
+ * mailto:contact AT sonarsource DOT com
+ *
+ * This program 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.
+ *
+ * This program 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.computation.task.projectanalysis.webhook;
+
+import javax.annotation.concurrent.Immutable;
+
+import static java.util.Objects.requireNonNull;
+
+@Immutable
+public class Webhook {
+
+  private final String name;
+  private final String url;
+
+  public Webhook(String name, String url) {
+    this.name = requireNonNull(name);
+    this.url = requireNonNull(url);
+  }
+
+  public String getName() {
+    return name;
+  }
+
+  public String getUrl() {
+    return url;
+  }
+}
diff --git a/server/sonar-server/src/main/java/org/sonar/server/computation/task/projectanalysis/webhook/WebhookCaller.java b/server/sonar-server/src/main/java/org/sonar/server/computation/task/projectanalysis/webhook/WebhookCaller.java
new file mode 100644 (file)
index 0000000..b931673
--- /dev/null
@@ -0,0 +1,34 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2016 SonarSource SA
+ * mailto:contact AT sonarsource DOT com
+ *
+ * This program 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.
+ *
+ * This program 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.computation.task.projectanalysis.webhook;
+
+public interface WebhookCaller {
+
+  /**
+   * Call webhook by sending a HTTP(S) POST request containing
+   * the JSON payload.
+   * <br/>
+   * Errors are silently ignored. They don't generate logs or
+   * throw exceptions. The error status is stored in the
+   * returned {@link WebhookDelivery}.
+   */
+  WebhookDelivery call(Webhook webhook, WebhookPayload payload);
+
+}
diff --git a/server/sonar-server/src/main/java/org/sonar/server/computation/task/projectanalysis/webhook/WebhookCallerImpl.java b/server/sonar-server/src/main/java/org/sonar/server/computation/task/projectanalysis/webhook/WebhookCallerImpl.java
new file mode 100644 (file)
index 0000000..26f618b
--- /dev/null
@@ -0,0 +1,69 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2016 SonarSource SA
+ * mailto:contact AT sonarsource DOT com
+ *
+ * This program 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.
+ *
+ * This program 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.computation.task.projectanalysis.webhook;
+
+import java.io.IOException;
+import okhttp3.MediaType;
+import okhttp3.OkHttpClient;
+import okhttp3.Request;
+import okhttp3.RequestBody;
+import okhttp3.Response;
+import org.sonar.api.ce.ComputeEngineSide;
+import org.sonar.api.utils.System2;
+
+@ComputeEngineSide
+public class WebhookCallerImpl implements WebhookCaller {
+
+  private static final MediaType JSON = MediaType.parse("application/json; charset=utf-8");
+  private static final String PROJECT_KEY_HEADER = "X-SonarQube-Project";
+
+  private final System2 system;
+  private final OkHttpClient okHttpClient;
+
+  public WebhookCallerImpl(System2 system, OkHttpClient okHttpClient) {
+    this.system = system;
+    this.okHttpClient = okHttpClient;
+  }
+
+  @Override
+  public WebhookDelivery call(Webhook webhook, WebhookPayload payload) {
+    Request.Builder request = new Request.Builder();
+    request.url(webhook.getUrl());
+    request.header(PROJECT_KEY_HEADER, payload.getProjectKey());
+    RequestBody body = RequestBody.create(JSON, payload.toJson());
+    request.post(body);
+
+    WebhookDelivery.Builder builder = new WebhookDelivery.Builder();
+    long startedAt = system.now();
+    builder
+      .setAt(startedAt)
+      .setPayload(payload)
+      .setWebhook(webhook);
+    try {
+      Response response = okHttpClient.newCall(request.build()).execute();
+      builder.setHttpStatus(response.code());
+      builder.setDurationInMs(system.now() - startedAt);
+    } catch (IOException e) {
+      builder.setThrowable(e);
+    }
+    return builder.build();
+  }
+
+}
diff --git a/server/sonar-server/src/main/java/org/sonar/server/computation/task/projectanalysis/webhook/WebhookDelivery.java b/server/sonar-server/src/main/java/org/sonar/server/computation/task/projectanalysis/webhook/WebhookDelivery.java
new file mode 100644 (file)
index 0000000..a262345
--- /dev/null
@@ -0,0 +1,131 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2016 SonarSource SA
+ * mailto:contact AT sonarsource DOT com
+ *
+ * This program 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.
+ *
+ * This program 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.computation.task.projectanalysis.webhook;
+
+import java.util.Optional;
+import javax.annotation.Nullable;
+import javax.annotation.concurrent.Immutable;
+
+import static java.util.Objects.requireNonNull;
+
+/**
+ * A {@link WebhookDelivery} represents the result of a webhook call.
+ */
+@Immutable
+public class WebhookDelivery {
+
+  private final Webhook webhook;
+  private final WebhookPayload payload;
+  private final Integer httpStatus;
+  private final Long durationInMs;
+  private final long at;
+  private final Throwable throwable;
+
+  private WebhookDelivery(Builder builder) {
+    this.webhook = requireNonNull(builder.webhook);
+    this.payload = requireNonNull(builder.payload);
+    this.httpStatus = builder.httpStatus;
+    this.durationInMs = builder.durationInMs;
+    this.at = builder.at;
+    this.throwable = builder.throwable;
+  }
+
+  public Webhook getWebhook() {
+    return webhook;
+  }
+
+  public WebhookPayload getPayload() {
+    return payload;
+  }
+
+  /**
+   * @return the HTTP status if {@link #getThrowable()} is empty, else returns 
+   * {@link Optional#empty()}
+   */
+  public Optional<Integer> getHttpStatus() {
+    return Optional.ofNullable(httpStatus);
+  }
+
+  /**
+   * @return the duration in milliseconds if {@link #getThrowable()} is empty,
+   * else returns {@link Optional#empty()}
+   */
+  public Optional<Long> getDurationInMs() {
+    return Optional.ofNullable(durationInMs);
+  }
+
+  /**
+   * @return the date of sending
+   */
+  public long getAt() {
+    return at;
+  }
+
+  /**
+   * @return the error raised if the request could not be executed due to a connectivity
+   * problem or timeout
+   */
+  public Optional<Throwable> getThrowable() {
+    return Optional.ofNullable(throwable);
+  }
+
+  public static class Builder {
+    private Webhook webhook;
+    private WebhookPayload payload;
+    private Integer httpStatus;
+    private Long durationInMs;
+    private long at;
+    private Throwable throwable;
+
+    public Builder setWebhook(Webhook w) {
+      this.webhook = w;
+      return this;
+    }
+
+    public Builder setPayload(WebhookPayload payload) {
+      this.payload = payload;
+      return this;
+    }
+
+    public Builder setHttpStatus(@Nullable Integer httpStatus) {
+      this.httpStatus = httpStatus;
+      return this;
+    }
+
+    public Builder setDurationInMs(@Nullable Long durationInMs) {
+      this.durationInMs = durationInMs;
+      return this;
+    }
+
+    public Builder setAt(long at) {
+      this.at = at;
+      return this;
+    }
+
+    public Builder setThrowable(@Nullable Throwable t) {
+      this.throwable = t;
+      return this;
+    }
+
+    public WebhookDelivery build() {
+      return new WebhookDelivery(this);
+    }
+  }
+}
diff --git a/server/sonar-server/src/main/java/org/sonar/server/computation/task/projectanalysis/webhook/WebhookModule.java b/server/sonar-server/src/main/java/org/sonar/server/computation/task/projectanalysis/webhook/WebhookModule.java
new file mode 100644 (file)
index 0000000..1bc200a
--- /dev/null
@@ -0,0 +1,31 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2016 SonarSource SA
+ * mailto:contact AT sonarsource DOT com
+ *
+ * This program 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.
+ *
+ * This program 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.computation.task.projectanalysis.webhook;
+
+import org.sonar.core.platform.Module;
+
+public class WebhookModule extends Module {
+  @Override
+  protected void configureModule() {
+    add(
+      WebhookCallerImpl.class,
+      WebhookPostTask.class);
+  }
+}
diff --git a/server/sonar-server/src/main/java/org/sonar/server/computation/task/projectanalysis/webhook/WebhookPayload.java b/server/sonar-server/src/main/java/org/sonar/server/computation/task/projectanalysis/webhook/WebhookPayload.java
new file mode 100644 (file)
index 0000000..2429b12
--- /dev/null
@@ -0,0 +1,107 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2016 SonarSource SA
+ * mailto:contact AT sonarsource DOT com
+ *
+ * This program 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.
+ *
+ * This program 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.computation.task.projectanalysis.webhook;
+
+import java.io.StringWriter;
+import java.io.Writer;
+import java.util.Date;
+import java.util.Optional;
+import javax.annotation.Nullable;
+import javax.annotation.concurrent.Immutable;
+import org.sonar.api.ce.posttask.CeTask;
+import org.sonar.api.ce.posttask.PostProjectAnalysisTask;
+import org.sonar.api.ce.posttask.Project;
+import org.sonar.api.ce.posttask.QualityGate;
+import org.sonar.api.utils.text.JsonWriter;
+
+import static java.util.Objects.requireNonNull;
+
+@Immutable
+public class WebhookPayload {
+
+  private final String projectKey;
+  private final String json;
+
+  public WebhookPayload(String projectKey, String json) {
+    this.projectKey = requireNonNull(projectKey);
+    this.json = requireNonNull(json);
+  }
+
+  public String getProjectKey() {
+    return projectKey;
+  }
+
+  public String toJson() {
+    return json;
+  }
+
+  public static WebhookPayload from(PostProjectAnalysisTask.ProjectAnalysis analysis) {
+    Writer string = new StringWriter();
+    JsonWriter writer = JsonWriter.of(string);
+    writer.beginObject();
+    writeTask(writer, analysis.getCeTask());
+    Optional<Date> analysisDate = analysis.getAnalysisDate();
+    if (analysisDate.isPresent()) {
+      writer.propDateTime("analysedAt", analysisDate.get());
+    }
+    writeProject(analysis, writer, analysis.getProject());
+    writeQualityGate(writer, analysis.getQualityGate());
+    writer.endObject().close();
+    return new WebhookPayload(analysis.getProject().getKey(), string.toString());
+  }
+
+  private static void writeTask(JsonWriter writer, CeTask ceTask) {
+    writer.prop("taskId", ceTask.getId());
+    writer.prop("status", ceTask.getStatus().toString());
+  }
+
+  private static void writeProject(PostProjectAnalysisTask.ProjectAnalysis analysis, JsonWriter writer, Project project) {
+    writer.name("project");
+    writer.beginObject();
+    writer.prop("key", project.getKey());
+    writer.prop("name", analysis.getProject().getName());
+    writer.endObject();
+  }
+
+  private static void writeQualityGate(JsonWriter writer, @Nullable QualityGate gate) {
+    if (gate != null) {
+      writer.name("qualityGate");
+      writer.beginObject();
+      writer.prop("name", gate.getName());
+      writer.prop("status", gate.getStatus().toString());
+      writer.name("conditions").beginArray();
+      for (QualityGate.Condition condition : gate.getConditions()) {
+        writer.beginObject();
+        writer.prop("metric", condition.getMetricKey());
+        writer.prop("operator", condition.getOperator().name());
+        if (condition.getStatus() != QualityGate.EvaluationStatus.NO_VALUE) {
+          writer.prop("value", condition.getValue());
+        }
+        writer.prop("status", condition.getStatus().name());
+        writer.prop("onLeakPeriod", condition.isOnLeakPeriod());
+        writer.prop("errorThreshold", condition.getErrorThreshold());
+        writer.prop("warningThreshold", condition.getWarningThreshold());
+        writer.endObject();
+      }
+      writer.endArray();
+      writer.endObject();
+    }
+  }
+}
diff --git a/server/sonar-server/src/main/java/org/sonar/server/computation/task/projectanalysis/webhook/WebhookPostTask.java b/server/sonar-server/src/main/java/org/sonar/server/computation/task/projectanalysis/webhook/WebhookPostTask.java
new file mode 100644 (file)
index 0000000..8ec33dd
--- /dev/null
@@ -0,0 +1,96 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2016 SonarSource SA
+ * mailto:contact AT sonarsource DOT com
+ *
+ * This program 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.
+ *
+ * This program 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.computation.task.projectanalysis.webhook;
+
+import com.google.common.collect.Iterables;
+import java.util.Arrays;
+import java.util.List;
+import java.util.Optional;
+import org.sonar.api.ce.posttask.PostProjectAnalysisTask;
+import org.sonar.api.config.Settings;
+import org.sonar.api.utils.log.Logger;
+import org.sonar.api.utils.log.Loggers;
+import org.sonar.core.config.WebhookProperties;
+import org.sonar.core.util.stream.Collectors;
+import org.sonar.server.computation.task.projectanalysis.component.SettingsRepository;
+import org.sonar.server.computation.task.projectanalysis.component.TreeRootHolder;
+
+import static com.google.common.base.Throwables.getRootCause;
+import static java.lang.String.format;
+
+public class WebhookPostTask implements PostProjectAnalysisTask {
+
+  private static final Logger LOGGER = Loggers.get(WebhookPostTask.class);
+
+  private final TreeRootHolder rootHolder;
+  private final SettingsRepository settingsRepository;
+  private final WebhookCaller caller;
+
+  public WebhookPostTask(TreeRootHolder rootHolder, SettingsRepository settingsRepository, WebhookCaller caller) {
+    this.rootHolder = rootHolder;
+    this.settingsRepository = settingsRepository;
+    this.caller = caller;
+  }
+
+  @Override
+  public void finished(ProjectAnalysis analysis) {
+    Settings settings = settingsRepository.getSettings(rootHolder.getRoot());
+
+    Iterable<String> webhookProps = Iterables.concat(
+      getWebhookProperties(settings, WebhookProperties.GLOBAL_KEY),
+      getWebhookProperties(settings, WebhookProperties.PROJECT_KEY)
+    );
+    if (!Iterables.isEmpty(webhookProps)) {
+      process(settings, analysis, webhookProps);
+    }
+  }
+
+  private static List<String> getWebhookProperties(Settings settings, String propertyKey) {
+    String[] webhookIds = settings.getStringArray(propertyKey);
+    return Arrays.stream(webhookIds)
+      .map(webhookId -> format("%s.%s", propertyKey, webhookId))
+      .collect(Collectors.toList(webhookIds.length));
+  }
+
+  private void process(Settings settings, ProjectAnalysis analysis, Iterable<String> webhookProperties) {
+    WebhookPayload payload = WebhookPayload.from(analysis);
+    for (String webhookProp : webhookProperties) {
+      String name = settings.getString(format("%s.%s", webhookProp, WebhookProperties.NAME_FIELD));
+      String url = settings.getString(format("%s.%s", webhookProp, WebhookProperties.URL_FIELD));
+      // as webhooks are defined as property sets, we can't ensure validity of fields on creation.
+      if (name != null && url != null) {
+        Webhook webhook = new Webhook(name, url);
+        WebhookDelivery delivery = caller.call(webhook, payload);
+        log(delivery);
+      }
+    }
+  }
+
+  private static void log(WebhookDelivery delivery) {
+    Optional<Throwable> throwable = delivery.getThrowable();
+    if (throwable.isPresent()) {
+      LOGGER.debug("Failed to send webhook '{}' | url={} | message={}",
+        delivery.getWebhook().getName(), delivery.getWebhook().getUrl(), getRootCause(throwable.get()).getMessage());
+    } else {
+      LOGGER.debug("Sent webhook '{}' | url={} | time={}ms | status={}",
+        delivery.getWebhook().getName(), delivery.getWebhook().getUrl(), delivery.getDurationInMs().orElse(-1L), delivery.getHttpStatus().orElse(-1));
+    }
+  }
+}
diff --git a/server/sonar-server/src/main/java/org/sonar/server/computation/task/projectanalysis/webhook/package-info.java b/server/sonar-server/src/main/java/org/sonar/server/computation/task/projectanalysis/webhook/package-info.java
new file mode 100644 (file)
index 0000000..7ef8477
--- /dev/null
@@ -0,0 +1,23 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2016 SonarSource SA
+ * mailto:contact AT sonarsource DOT com
+ *
+ * This program 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.
+ *
+ * This program 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.
+ */
+@ParametersAreNonnullByDefault
+package org.sonar.server.computation.task.projectanalysis.webhook;
+
+import javax.annotation.ParametersAreNonnullByDefault;
diff --git a/server/sonar-server/src/test/java/org/sonar/server/computation/task/projectanalysis/component/TestSettingsRepository.java b/server/sonar-server/src/test/java/org/sonar/server/computation/task/projectanalysis/component/TestSettingsRepository.java
new file mode 100644 (file)
index 0000000..1973ebb
--- /dev/null
@@ -0,0 +1,40 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2016 SonarSource SA
+ * mailto:contact AT sonarsource DOT com
+ *
+ * This program 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.
+ *
+ * This program 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.computation.task.projectanalysis.component;
+
+import org.sonar.api.config.Settings;
+
+/**
+ * Implementation of {@link SettingsRepository} that always return the
+ * same mutable {@link Settings}, whatever the component.
+ */
+public class TestSettingsRepository implements SettingsRepository {
+
+  private final Settings settings;
+
+  public TestSettingsRepository(Settings settings) {
+    this.settings = settings;
+  }
+
+  @Override
+  public Settings getSettings(Component component) {
+    return settings;
+  }
+}
diff --git a/server/sonar-server/src/test/java/org/sonar/server/computation/task/projectanalysis/webhook/TestWebhookCaller.java b/server/sonar-server/src/test/java/org/sonar/server/computation/task/projectanalysis/webhook/TestWebhookCaller.java
new file mode 100644 (file)
index 0000000..1e82369
--- /dev/null
@@ -0,0 +1,75 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2016 SonarSource SA
+ * mailto:contact AT sonarsource DOT com
+ *
+ * This program 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.
+ *
+ * This program 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.computation.task.projectanalysis.webhook;
+
+import java.util.LinkedList;
+import java.util.Queue;
+import java.util.concurrent.atomic.AtomicInteger;
+import javax.annotation.Nullable;
+
+import static java.util.Objects.requireNonNull;
+
+public class TestWebhookCaller implements WebhookCaller {
+
+  private final Queue<Item> deliveries = new LinkedList<>();
+  private final AtomicInteger countSent = new AtomicInteger(0);
+
+  public TestWebhookCaller enqueueSuccess(long at, int httpCode, long durationMs) {
+    deliveries.add(new Item(at, httpCode, durationMs, null));
+    return this;
+  }
+
+  public TestWebhookCaller enqueueFailure(long at, Throwable t) {
+    deliveries.add(new Item(at, null, null, t));
+    return this;
+  }
+
+  @Override
+  public WebhookDelivery call(Webhook webhook, WebhookPayload payload) {
+    Item item = requireNonNull(deliveries.poll(), "Queue is empty");
+    countSent.incrementAndGet();
+    return new WebhookDelivery.Builder()
+      .setAt(item.at)
+      .setHttpStatus(item.httpCode)
+      .setDurationInMs(item.durationMs)
+      .setThrowable(item.throwable)
+      .setPayload(payload)
+      .setWebhook(webhook)
+      .build();
+  }
+
+  public int countSent() {
+    return countSent.get();
+  }
+
+  private static class Item {
+    final long at;
+    final Integer httpCode;
+    final Long durationMs;
+    final Throwable throwable;
+
+    Item(long at, @Nullable Integer httpCode, @Nullable Long durationMs, @Nullable Throwable throwable) {
+      this.at = at;
+      this.httpCode = httpCode;
+      this.durationMs = durationMs;
+      this.throwable = throwable;
+    }
+  }
+}
diff --git a/server/sonar-server/src/test/java/org/sonar/server/computation/task/projectanalysis/webhook/WebhookCallerImplTest.java b/server/sonar-server/src/test/java/org/sonar/server/computation/task/projectanalysis/webhook/WebhookCallerImplTest.java
new file mode 100644 (file)
index 0000000..474974f
--- /dev/null
@@ -0,0 +1,92 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2016 SonarSource SA
+ * mailto:contact AT sonarsource DOT com
+ *
+ * This program 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.
+ *
+ * This program 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.computation.task.projectanalysis.webhook;
+
+import okhttp3.mockwebserver.MockResponse;
+import okhttp3.mockwebserver.MockWebServer;
+import okhttp3.mockwebserver.RecordedRequest;
+import org.junit.Rule;
+import org.junit.Test;
+import org.sonar.api.SonarQubeSide;
+import org.sonar.api.SonarRuntime;
+import org.sonar.api.config.MapSettings;
+import org.sonar.api.internal.SonarRuntimeImpl;
+import org.sonar.api.utils.System2;
+import org.sonar.api.utils.Version;
+import org.sonar.api.utils.internal.TestSystem2;
+import org.sonar.server.util.OkHttpClientProvider;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+
+public class WebhookCallerImplTest {
+
+  private static final long NOW = 1_500_000_000_000L;
+
+  @Rule
+  public MockWebServer server = new MockWebServer();
+
+  private System2 system = new TestSystem2().setNow(NOW);
+
+  @Test
+  public void send_posts_payload_to_http_server() throws Exception {
+    Webhook webhook = new Webhook("my-webhook", server.url("/ping").toString());
+    WebhookPayload payload = new WebhookPayload("P1", "{the payload}");
+
+    server.enqueue(new MockResponse().setBody("pong").setResponseCode(201));
+    WebhookDelivery delivery = newSender().call(webhook, payload);
+
+    assertThat(delivery.getHttpStatus().get()).isEqualTo(201);
+    assertThat(delivery.getDurationInMs().get()).isGreaterThanOrEqualTo(0L);
+    assertThat(delivery.getThrowable()).isEmpty();
+    assertThat(delivery.getAt()).isEqualTo(NOW);
+    assertThat(delivery.getWebhook()).isSameAs(webhook);
+    assertThat(delivery.getPayload()).isSameAs(payload);
+
+    RecordedRequest recordedRequest = server.takeRequest();
+    assertThat(recordedRequest.getMethod()).isEqualTo("POST");
+    assertThat(recordedRequest.getPath()).isEqualTo("/ping");
+    assertThat(recordedRequest.getBody().readUtf8()).isEqualTo(payload.toJson());
+    assertThat(recordedRequest.getHeader("User-Agent")).isEqualTo("SonarQube/6.2");
+    assertThat(recordedRequest.getHeader("Content-Type")).isEqualTo("application/json; charset=utf-8");
+    assertThat(recordedRequest.getHeader("X-SonarQube-Project")).isEqualTo(payload.getProjectKey());
+  }
+
+  @Test
+  public void send_does_not_throw_exception_on_errors() throws Exception {
+    Webhook webhook = new Webhook("my-webhook", server.url("/ping").toString());
+    WebhookPayload payload = new WebhookPayload("P1", "{the payload}");
+
+    server.shutdown();
+    WebhookDelivery delivery = newSender().call(webhook, payload);
+
+    assertThat(delivery.getHttpStatus()).isEmpty();
+    assertThat(delivery.getDurationInMs()).isEmpty();
+    assertThat(delivery.getThrowable().get().getMessage()).startsWith("Failed to connect to");
+    assertThat(delivery.getAt()).isEqualTo(NOW);
+    assertThat(delivery.getWebhook()).isSameAs(webhook);
+    assertThat(delivery.getPayload()).isSameAs(payload);
+  }
+
+  private WebhookCaller newSender() {
+    SonarRuntime runtime = SonarRuntimeImpl.forSonarQube(Version.parse("6.2"), SonarQubeSide.SERVER);
+    return new WebhookCallerImpl(system, new OkHttpClientProvider().provide(new MapSettings(), runtime));
+  }
+}
diff --git a/server/sonar-server/src/test/java/org/sonar/server/computation/task/projectanalysis/webhook/WebhookPayloadTest.java b/server/sonar-server/src/test/java/org/sonar/server/computation/task/projectanalysis/webhook/WebhookPayloadTest.java
new file mode 100644 (file)
index 0000000..f01d81d
--- /dev/null
@@ -0,0 +1,142 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2016 SonarSource SA
+ * mailto:contact AT sonarsource DOT com
+ *
+ * This program 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.
+ *
+ * This program 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.computation.task.projectanalysis.webhook;
+
+import java.util.Date;
+import java.util.Optional;
+import javax.annotation.Nullable;
+import org.junit.Test;
+import org.sonar.api.ce.posttask.CeTask;
+import org.sonar.api.ce.posttask.PostProjectAnalysisTask;
+import org.sonar.api.ce.posttask.Project;
+import org.sonar.api.ce.posttask.QualityGate;
+import org.sonar.api.ce.posttask.ScannerContext;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.sonar.api.ce.posttask.PostProjectAnalysisTaskTester.newCeTaskBuilder;
+import static org.sonar.api.ce.posttask.PostProjectAnalysisTaskTester.newConditionBuilder;
+import static org.sonar.api.ce.posttask.PostProjectAnalysisTaskTester.newProjectBuilder;
+import static org.sonar.api.ce.posttask.PostProjectAnalysisTaskTester.newQualityGateBuilder;
+import static org.sonar.api.ce.posttask.PostProjectAnalysisTaskTester.newScannerContextBuilder;
+import static org.sonar.test.JsonAssert.assertJson;
+
+public class WebhookPayloadTest {
+
+  private static final String PROJECT_KEY = "P1";
+
+  @Test
+  public void create_payload_for_successful_analysis() {
+    CeTask task = newCeTaskBuilder()
+      .setStatus(CeTask.Status.SUCCESS)
+      .setId("#1")
+      .build();
+    QualityGate gate = newQualityGateBuilder()
+      .setId("G1")
+      .setName("Gate One")
+      .setStatus(QualityGate.Status.WARN)
+      .add(newConditionBuilder()
+        .setMetricKey("coverage")
+        .setOperator(QualityGate.Operator.GREATER_THAN)
+        .setOnLeakPeriod(true)
+        .setWarningThreshold("75.0")
+        .setErrorThreshold("70.0")
+        .build(QualityGate.EvaluationStatus.WARN, "74.0"))
+      .build();
+    PostProjectAnalysisTask.ProjectAnalysis analysis = newAnalysis(task, gate);
+
+    WebhookPayload payload = WebhookPayload.from(analysis);
+    assertThat(payload.getProjectKey()).isEqualTo(PROJECT_KEY);
+    assertJson(payload.toJson()).isSimilarTo(getClass().getResource("WebhookPayloadTest/success.json"));
+  }
+
+  @Test
+  public void create_payload_for_failed_analysis() {
+    CeTask ceTask = newCeTaskBuilder().setStatus(CeTask.Status.FAILED).setId("#1").build();
+    PostProjectAnalysisTask.ProjectAnalysis analysis = newAnalysis(ceTask, null);
+
+    WebhookPayload payload = WebhookPayload.from(analysis);
+
+    assertThat(payload.getProjectKey()).isEqualTo(PROJECT_KEY);
+    assertJson(payload.toJson()).isSimilarTo(getClass().getResource("WebhookPayloadTest/failed.json"));
+  }
+
+  @Test
+  public void create_payload_with_gate_conditions_without_value() {
+    CeTask task = newCeTaskBuilder()
+      .setStatus(CeTask.Status.SUCCESS)
+      .setId("#1")
+      .build();
+    QualityGate gate = newQualityGateBuilder()
+      .setId("G1")
+      .setName("Gate One")
+      .setStatus(QualityGate.Status.WARN)
+      .add(newConditionBuilder()
+        .setMetricKey("coverage")
+        .setOperator(QualityGate.Operator.GREATER_THAN)
+        .setWarningThreshold("75.0")
+        .setErrorThreshold("70.0")
+        .buildNoValue())
+      .build();
+    PostProjectAnalysisTask.ProjectAnalysis analysis = newAnalysis(task, gate);
+
+    WebhookPayload payload = WebhookPayload.from(analysis);
+    assertThat(payload.getProjectKey()).isEqualTo(PROJECT_KEY);
+    assertJson(payload.toJson()).isSimilarTo(getClass().getResource("WebhookPayloadTest/gate_condition_without_value.json"));
+  }
+
+  private static PostProjectAnalysisTask.ProjectAnalysis newAnalysis(CeTask task, @Nullable QualityGate gate) {
+    return new PostProjectAnalysisTask.ProjectAnalysis() {
+      @Override
+      public CeTask getCeTask() {
+        return task;
+      }
+
+      @Override
+      public Project getProject() {
+        return newProjectBuilder()
+          .setUuid("P1_UUID")
+          .setKey(PROJECT_KEY)
+          .setName("Project One")
+          .build();
+      }
+
+      @Override
+      public QualityGate getQualityGate() {
+        return gate;
+      }
+
+      @Override
+      public Date getDate() {
+        throw new UnsupportedOperationException();
+      }
+
+      @Override
+      public Optional<Date> getAnalysisDate() {
+        return Optional.of(new Date(1_500_000_000_000L));
+      }
+
+      @Override
+      public ScannerContext getScannerContext() {
+        return newScannerContextBuilder().build();
+      }
+    };
+  }
+
+}
diff --git a/server/sonar-server/src/test/java/org/sonar/server/computation/task/projectanalysis/webhook/WebhookPostTaskTest.java b/server/sonar-server/src/test/java/org/sonar/server/computation/task/projectanalysis/webhook/WebhookPostTaskTest.java
new file mode 100644 (file)
index 0000000..ac8224c
--- /dev/null
@@ -0,0 +1,107 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2016 SonarSource SA
+ * mailto:contact AT sonarsource DOT com
+ *
+ * This program 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.
+ *
+ * This program 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.computation.task.projectanalysis.webhook;
+
+import java.io.IOException;
+import java.util.Date;
+import org.junit.Rule;
+import org.junit.Test;
+import org.sonar.api.ce.posttask.CeTask;
+import org.sonar.api.ce.posttask.PostProjectAnalysisTaskTester;
+import org.sonar.api.config.MapSettings;
+import org.sonar.api.utils.log.LogTester;
+import org.sonar.api.utils.log.LoggerLevel;
+import org.sonar.server.computation.task.projectanalysis.component.SettingsRepository;
+import org.sonar.server.computation.task.projectanalysis.component.TestSettingsRepository;
+import org.sonar.server.computation.task.projectanalysis.component.TreeRootHolderRule;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.sonar.api.ce.posttask.PostProjectAnalysisTaskTester.newCeTaskBuilder;
+import static org.sonar.api.ce.posttask.PostProjectAnalysisTaskTester.newProjectBuilder;
+import static org.sonar.server.computation.task.projectanalysis.component.ReportComponent.DUMB_PROJECT;
+
+public class WebhookPostTaskTest {
+
+  private static final long NOW = 1_500_000_000_000L;
+
+  @Rule
+  public LogTester logTester = new LogTester().setLevel(LoggerLevel.DEBUG);
+
+  @Rule
+  public TreeRootHolderRule rootHolder = new TreeRootHolderRule().setRoot(DUMB_PROJECT);
+
+  private final MapSettings settings = new MapSettings();
+  private final TestWebhookCaller caller = new TestWebhookCaller();
+
+  @Test
+  public void do_nothing_if_no_webhooks() {
+    execute();
+
+    assertThat(caller.countSent()).isEqualTo(0);
+    assertThat(logTester.logs(LoggerLevel.DEBUG)).isEmpty();
+  }
+
+  @Test
+  public void send_global_webhooks() {
+    settings.setProperty("sonar.webhooks.global", "1,2");
+    settings.setProperty("sonar.webhooks.global.1.name", "First");
+    settings.setProperty("sonar.webhooks.global.1.url", "http://url1");
+    settings.setProperty("sonar.webhooks.global.2.name", "Second");
+    settings.setProperty("sonar.webhooks.global.2.url", "http://url2");
+    caller.enqueueSuccess(NOW, 200, 1_234);
+    caller.enqueueFailure(NOW, new IOException("Fail to connect"));
+
+    execute();
+
+    assertThat(caller.countSent()).isEqualTo(2);
+    assertThat(logTester.logs(LoggerLevel.DEBUG)).contains("Sent webhook 'First' | url=http://url1 | time=1234ms | status=200");
+    assertThat(logTester.logs(LoggerLevel.DEBUG)).contains("Failed to send webhook 'Second' | url=http://url2 | message=Fail to connect");
+  }
+  @Test
+  public void send_project_webhooks() {
+    settings.setProperty("sonar.webhooks.project", "1");
+    settings.setProperty("sonar.webhooks.project.1.name", "First");
+    settings.setProperty("sonar.webhooks.project.1.url", "http://url1");
+    caller.enqueueSuccess(NOW, 200, 1_234);
+
+    execute();
+
+    assertThat(caller.countSent()).isEqualTo(1);
+    assertThat(logTester.logs(LoggerLevel.DEBUG)).contains("Sent webhook 'First' | url=http://url1 | time=1234ms | status=200");
+  }
+
+  private void execute() {
+    SettingsRepository settingsRepository = new TestSettingsRepository(settings);
+    WebhookPostTask task = new WebhookPostTask(rootHolder, settingsRepository, caller);
+
+    PostProjectAnalysisTaskTester.of(task)
+      .at(new Date())
+      .withCeTask(newCeTaskBuilder()
+        .setStatus(CeTask.Status.SUCCESS)
+        .setId("#1")
+        .build())
+      .withProject(newProjectBuilder()
+        .setUuid("P1_UUID")
+        .setKey("P1")
+        .setName("Project One")
+        .build())
+      .execute();
+  }
+}
diff --git a/server/sonar-server/src/test/resources/org/sonar/server/computation/task/projectanalysis/webhook/WebhookPayloadTest/failed.json b/server/sonar-server/src/test/resources/org/sonar/server/computation/task/projectanalysis/webhook/WebhookPayloadTest/failed.json
new file mode 100644 (file)
index 0000000..358c1f0
--- /dev/null
@@ -0,0 +1,8 @@
+{
+  "taskId": "#1",
+  "status": "FAILED",
+  "project": {
+    "key": "P1",
+    "name": "Project One"
+  }
+}
diff --git a/server/sonar-server/src/test/resources/org/sonar/server/computation/task/projectanalysis/webhook/WebhookPayloadTest/gate_condition_without_value.json b/server/sonar-server/src/test/resources/org/sonar/server/computation/task/projectanalysis/webhook/WebhookPayloadTest/gate_condition_without_value.json
new file mode 100644 (file)
index 0000000..7cdcfc9
--- /dev/null
@@ -0,0 +1,23 @@
+{
+  "taskId": "#1",
+  "status": "SUCCESS",
+  "analysedAt": "2017-07-14T04:40:00+0200",
+  "project": {
+    "key": "P1",
+    "name": "Project One"
+  },
+  "qualityGate": {
+    "name": "Gate One",
+    "status": "WARN",
+    "conditions": [
+      {
+        "metric": "coverage",
+        "operator": "GREATER_THAN",
+        "status": "NO_VALUE",
+        "onLeakPeriod": false,
+        "errorThreshold": "70.0",
+        "warningThreshold": "75.0"
+      }
+    ]
+  }
+}
diff --git a/server/sonar-server/src/test/resources/org/sonar/server/computation/task/projectanalysis/webhook/WebhookPayloadTest/success.json b/server/sonar-server/src/test/resources/org/sonar/server/computation/task/projectanalysis/webhook/WebhookPayloadTest/success.json
new file mode 100644 (file)
index 0000000..fdb7f8e
--- /dev/null
@@ -0,0 +1,24 @@
+{
+  "taskId": "#1",
+  "status": "SUCCESS",
+  "analysedAt": "2017-07-14T04:40:00+0200",
+  "project": {
+    "key": "P1",
+    "name": "Project One"
+  },
+  "qualityGate": {
+    "name": "Gate One",
+    "status": "WARN",
+    "conditions": [
+      {
+        "metric": "coverage",
+        "operator": "GREATER_THAN",
+        "value": "74.0",
+        "status": "WARN",
+        "onLeakPeriod": true,
+        "errorThreshold": "70.0",
+        "warningThreshold": "75.0"
+      }
+    ]
+  }
+}
index d4051edd6f3879a2a6c2e9fec3779ad688381b93..82c7440466008b722c9fed6a97561010ca219c1a 100644 (file)
@@ -62,6 +62,7 @@ public class CorePropertyDefinitions {
     defs.addAll(DebtProperties.all());
     defs.addAll(PurgeProperties.all());
     defs.addAll(EmailSettings.definitions());
+    defs.addAll(WebhookProperties.all());
 
     defs.addAll(ImmutableList.of(
       PropertyDefinition.builder(PROP_PASSWORD)
diff --git a/sonar-core/src/main/java/org/sonar/core/config/WebhookProperties.java b/sonar-core/src/main/java/org/sonar/core/config/WebhookProperties.java
new file mode 100644 (file)
index 0000000..6c2fb50
--- /dev/null
@@ -0,0 +1,77 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2016 SonarSource SA
+ * mailto:contact AT sonarsource DOT com
+ *
+ * This program 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.
+ *
+ * This program 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.core.config;
+
+import com.google.common.collect.ImmutableList;
+import java.util.List;
+import org.sonar.api.PropertyType;
+import org.sonar.api.config.PropertyDefinition;
+import org.sonar.api.config.PropertyFieldDefinition;
+import org.sonar.api.resources.Qualifiers;
+
+public class WebhookProperties {
+
+  public static final String GLOBAL_KEY = "sonar.webhooks.global";
+  public static final String PROJECT_KEY = "sonar.webhooks.project";
+  public static final String NAME_FIELD = "name";
+  public static final String URL_FIELD = "url";
+  private static final String CATEGORY = "webhooks";
+  private static final String DESCRIPTION = "Webhooks are used to notify external services when a project analysis is done. " +
+    "A HTTP POST request including a JSON payload is sent to each of the provided URLs. " +
+    "Learn more in the <a href=\"#\">Webhooks documentation</a>.";
+
+  private WebhookProperties() {
+    // only static stuff
+  }
+
+  static List<PropertyDefinition> all() {
+    return ImmutableList.of(
+      PropertyDefinition.builder(GLOBAL_KEY)
+        .category(CATEGORY)
+        .name("Webhooks")
+        .description(DESCRIPTION)
+        .fields(
+          PropertyFieldDefinition.build(NAME_FIELD)
+            .name("Name")
+            .type(PropertyType.STRING)
+            .build(),
+          PropertyFieldDefinition.build(URL_FIELD)
+            .name("URL")
+            .type(PropertyType.STRING)
+            .build())
+        .build(),
+
+      PropertyDefinition.builder(PROJECT_KEY)
+        .category(CATEGORY)
+        .name("Project Webhooks")
+        .description(DESCRIPTION)
+        .onlyOnQualifiers(Qualifiers.PROJECT)
+        .fields(
+          PropertyFieldDefinition.build(NAME_FIELD)
+            .name("Name")
+            .type(PropertyType.STRING)
+            .build(),
+          PropertyFieldDefinition.build(URL_FIELD)
+            .name("URL")
+            .type(PropertyType.STRING)
+            .build())
+        .build());
+  }
+}
index 76ab0fc2ad1b8d6a80fc331df5af5c35d7040cd4..f8ad062fc5706ea138c7b855abf640317e39f6ec 100644 (file)
@@ -893,6 +893,7 @@ property.category.duplications=Duplications
 property.category.localization=Localization
 property.category.server_id=Server ID
 property.category.exclusions=Analysis Scope
+property.category.webhooks=Webhooks
 property.sonar.inclusions.name=Source File Inclusions
 property.sonar.inclusions.description=Patterns used to include some source files and only these ones in analysis.
 property.sonar.test.inclusions.name=Test File Inclusions
index 4e66c693efbbba91d67a83a530a2dc97e53a33fe..bf9d2501497eb5f27d889b81f3d83b9904fd6a41 100644 (file)
@@ -35,7 +35,7 @@ public class CorePropertyDefinitionsTest {
   @Test
   public void all() {
     List<PropertyDefinition> defs = CorePropertyDefinitions.all();
-    assertThat(defs.size()).isGreaterThan(9);
+    assertThat(defs).hasSize(64);
   }
 
   @Test