]> source.dussan.org Git - sonarqube.git/commitdiff
SONAR-10266 Generate measure badge
authorJulien Lancelot <julien.lancelot@sonarsource.com>
Mon, 15 Jan 2018 17:08:50 +0000 (18:08 +0100)
committerGrégoire Aubert <gregoire.aubert@sonarsource.com>
Thu, 25 Jan 2018 14:16:50 +0000 (15:16 +0100)
13 files changed:
server/sonar-server/src/main/java/org/sonar/server/badge/ws/MeasureAction.java
server/sonar-server/src/main/java/org/sonar/server/badge/ws/ProjectBadgesException.java [new file with mode: 0644]
server/sonar-server/src/main/java/org/sonar/server/badge/ws/ProjectBadgesWsModule.java
server/sonar-server/src/main/java/org/sonar/server/badge/ws/SvgFormatter.java [new file with mode: 0644]
server/sonar-server/src/main/java/org/sonar/server/badge/ws/SvgGenerator.java [new file with mode: 0644]
server/sonar-server/src/main/resources/org/sonar/server/badge/ws/measure-example.svg
server/sonar-server/src/main/resources/org/sonar/server/badge/ws/quality_gate-example.svg
server/sonar-server/src/main/resources/org/sonar/server/badge/ws/templates/badge.svg [new file with mode: 0644]
server/sonar-server/src/main/resources/org/sonar/server/badge/ws/templates/error.svg [new file with mode: 0644]
server/sonar-server/src/test/java/org/sonar/server/badge/ws/MeasureActionTest.java
server/sonar-server/src/test/java/org/sonar/server/badge/ws/ProjectBadgesWsModuleTest.java
server/sonar-server/src/test/java/org/sonar/server/badge/ws/SvgFormatterTest.java [new file with mode: 0644]
sonar-ws/src/main/java/org/sonarqube/ws/MediaTypes.java

index 6c3fcfa72b03f1288f965873b64787992044b2f3..44d1b3fbc2d53b0945a939eb378f5e60c67dee56 100644 (file)
  */
 package org.sonar.server.badge.ws;
 
+import com.google.common.collect.ImmutableMap;
 import com.google.common.io.Resources;
-import org.apache.commons.io.IOUtils;
-import org.sonar.api.measures.CoreMetrics;
+import java.util.EnumMap;
+import java.util.Map;
+import java.util.function.Function;
 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.server.ws.WebService.NewAction;
+import org.sonar.db.DbClient;
+import org.sonar.db.DbSession;
+import org.sonar.db.component.ComponentDto;
+import org.sonar.db.measure.LiveMeasureDto;
+import org.sonar.db.metric.MetricDto;
+import org.sonar.server.badge.ws.SvgGenerator.Color;
+import org.sonar.server.component.ComponentFinder;
+import org.sonar.server.computation.task.projectanalysis.qualitymodel.Rating;
+import org.sonar.server.exceptions.ForbiddenException;
+import org.sonar.server.exceptions.NotFoundException;
+import org.sonar.server.user.UserSession;
 
+import static com.google.common.base.Preconditions.checkState;
+import static java.lang.String.format;
+import static java.nio.charset.StandardCharsets.UTF_8;
+import static org.apache.commons.io.IOUtils.write;
+import static org.sonar.api.measures.CoreMetrics.ALERT_STATUS_KEY;
+import static org.sonar.api.measures.CoreMetrics.BUGS_KEY;
+import static org.sonar.api.measures.CoreMetrics.CODE_SMELLS_KEY;
+import static org.sonar.api.measures.CoreMetrics.COVERAGE_KEY;
+import static org.sonar.api.measures.CoreMetrics.DUPLICATED_LINES_DENSITY_KEY;
+import static org.sonar.api.measures.CoreMetrics.NCLOC_KEY;
+import static org.sonar.api.measures.CoreMetrics.RELIABILITY_RATING_KEY;
+import static org.sonar.api.measures.CoreMetrics.SECURITY_RATING_KEY;
+import static org.sonar.api.measures.CoreMetrics.SQALE_RATING_KEY;
+import static org.sonar.api.measures.CoreMetrics.TECHNICAL_DEBT_KEY;
+import static org.sonar.api.measures.CoreMetrics.TESTS_KEY;
+import static org.sonar.api.measures.CoreMetrics.VULNERABILITIES_KEY;
+import static org.sonar.api.measures.Metric.Level;
+import static org.sonar.api.measures.Metric.ValueType;
+import static org.sonar.api.measures.Metric.Level.ERROR;
+import static org.sonar.api.measures.Metric.Level.OK;
+import static org.sonar.api.measures.Metric.Level.WARN;
+import static org.sonar.api.web.UserRole.USER;
+import static org.sonar.server.badge.ws.SvgFormatter.formatDuration;
+import static org.sonar.server.badge.ws.SvgFormatter.formatNumeric;
+import static org.sonar.server.badge.ws.SvgFormatter.formatPercent;
+import static org.sonar.server.computation.task.projectanalysis.qualitymodel.Rating.A;
+import static org.sonar.server.computation.task.projectanalysis.qualitymodel.Rating.B;
+import static org.sonar.server.computation.task.projectanalysis.qualitymodel.Rating.C;
+import static org.sonar.server.computation.task.projectanalysis.qualitymodel.Rating.D;
+import static org.sonar.server.computation.task.projectanalysis.qualitymodel.Rating.E;
+import static org.sonar.server.computation.task.projectanalysis.qualitymodel.Rating.valueOf;
+import static org.sonar.server.ws.KeyExamples.KEY_BRANCH_EXAMPLE_001;
 import static org.sonar.server.ws.KeyExamples.KEY_PROJECT_EXAMPLE_001;
+import static org.sonarqube.ws.MediaTypes.SVG;
 
 public class MeasureAction implements ProjectBadgesWsAction {
 
-  public static final String PARAM_COMPONENT = "component";
-  public static final String PARAM_METRIC = "metric";
+  private static final String PARAM_PROJECT = "project";
+  private static final String PARAM_BRANCH = "branch";
+  private static final String PARAM_METRIC = "metric";
+
+  private static final Map<String, String> METRIC_NAME_BY_KEY = ImmutableMap.<String, String>builder()
+    .put(ALERT_STATUS_KEY, "quality gate")
+    .put(COVERAGE_KEY, "coverage")
+    .put(RELIABILITY_RATING_KEY, "reliability")
+    .put(SECURITY_RATING_KEY, "security")
+    .put(SQALE_RATING_KEY, "maintainability")
+    .put(BUGS_KEY, "bugs")
+    .put(VULNERABILITIES_KEY, "vulnerabilities")
+    .put(CODE_SMELLS_KEY, "code smells")
+    .put(DUPLICATED_LINES_DENSITY_KEY, "duplicated lines")
+    .put(TECHNICAL_DEBT_KEY, "technical debt")
+    .put(TESTS_KEY, "unit tests")
+    .build();
+
+  private static final Map<Level, String> QUALITY_GATE_MESSAGE_BY_STATUS = new EnumMap<>(ImmutableMap.of(
+    OK, "passed",
+    WARN, "warning",
+    ERROR, "failed"));
+
+  private static final Map<Level, Color> COLOR_BY_QUALITY_GATE_STATUS = new EnumMap<>(ImmutableMap.of(
+    OK, Color.QUALITY_GATE_OK,
+    WARN, Color.QUALITY_GATE_WARN,
+    ERROR, Color.QUALITY_GATE_ERROR));
+
+  private static final Map<Rating, Color> COLOR_BY_RATING = new EnumMap<>(ImmutableMap.of(
+    A, Color.RATING_A,
+    B, Color.RATING_B,
+    C, Color.RATING_C,
+    D, Color.RATING_D,
+    E, Color.RATING_E));
+
+  private final UserSession userSession;
+  private final DbClient dbClient;
+  private final ComponentFinder componentFinder;
+  private final SvgGenerator svgGenerator;
+
+  public MeasureAction(UserSession userSession, DbClient dbClient, ComponentFinder componentFinder, SvgGenerator svgGenerator) {
+    this.userSession = userSession;
+    this.dbClient = dbClient;
+    this.componentFinder = componentFinder;
+    this.svgGenerator = svgGenerator;
+  }
 
   @Override
   public void define(WebService.NewController controller) {
     NewAction action = controller.createAction("measure")
       .setHandler(this)
-      .setDescription("Generate badge for measure as an SVG")
+      .setDescription("Generate badge for project's measure as an SVG.<br/>" +
+        "Requires 'Browse' permission on the specified project.")
+      .setSince("7.1")
       .setResponseExample(Resources.getResource(getClass(), "measure-example.svg"));
-    action.createParam(PARAM_COMPONENT)
+    action.createParam(PARAM_PROJECT)
       .setDescription("Project key")
       .setRequired(true)
       .setExampleValue(KEY_PROJECT_EXAMPLE_001);
+    action
+      .createParam(PARAM_BRANCH)
+      .setDescription("Branch key")
+      .setExampleValue(KEY_BRANCH_EXAMPLE_001);
     action.createParam(PARAM_METRIC)
       .setDescription("Metric key")
       .setRequired(true)
-      .setPossibleValues(
-        CoreMetrics.ALERT_STATUS_KEY,
-        CoreMetrics.COVERAGE_KEY,
-        CoreMetrics.RELIABILITY_RATING_KEY,
-        CoreMetrics.SECURITY_RATING_KEY,
-        CoreMetrics.SQALE_RATING_KEY,
-        CoreMetrics.BUGS_KEY,
-        CoreMetrics.VULNERABILITIES_KEY,
-        CoreMetrics.CODE_SMELLS_KEY,
-        CoreMetrics.DUPLICATED_LINES_DENSITY_KEY,
-        CoreMetrics.TECHNICAL_DEBT_KEY,
-        CoreMetrics.TESTS_KEY
-        )
-      .setExampleValue(KEY_PROJECT_EXAMPLE_001);
+      .setPossibleValues(METRIC_NAME_BY_KEY.keySet());
   }
 
   @Override
   public void handle(Request request, Response response) throws Exception {
-    response.stream().setMediaType("image/svg+xml");
-    IOUtils.copy(Resources.getResource(getClass(), "measure-example.svg").openStream(), response.stream().output());
+    response.stream().setMediaType(SVG);
+    String projectKey = request.mandatoryParam(PARAM_PROJECT);
+    String branch = request.param(PARAM_BRANCH);
+    String metricKey = request.mandatoryParam(PARAM_METRIC);
+    try (DbSession dbSession = dbClient.openSession(false)) {
+      ComponentDto project = componentFinder.getByKeyAndOptionalBranch(dbSession, projectKey, branch);
+      userSession.checkComponentPermission(USER, project);
+      MetricDto metric = dbClient.metricDao().selectByKey(dbSession, metricKey);
+      checkState(metric != null && metric.isEnabled(), "Metric '%s' hasn't been found", metricKey);
+      LiveMeasureDto measure = getMeasure(dbSession, project, metricKey);
+      write(generateSvg(metric, measure), response.stream().output(), UTF_8);
+    } catch (ProjectBadgesException | ForbiddenException | NotFoundException e) {
+      write(svgGenerator.generateError(e.getMessage()), response.stream().output(), UTF_8);
+    }
+  }
+
+  private LiveMeasureDto getMeasure(DbSession dbSession, ComponentDto project, String metricKey) {
+    return dbClient.liveMeasureDao().selectMeasure(dbSession, project.uuid(), metricKey)
+      .orElseThrow(() -> new ProjectBadgesException(format("Measure '%s' has not been found for project '%s' and branch '%s'", metricKey, project.getKey(), project.getBranch())));
+  }
+
+  private String generateSvg(MetricDto metric, LiveMeasureDto measure) {
+    String metricType = metric.getValueType();
+    switch (ValueType.valueOf(metricType)) {
+      case INT:
+        return generateBadge(metric, formatNumeric(getNonNullValue(measure, LiveMeasureDto::getValue).longValue()), Color.DEFAULT);
+      case PERCENT:
+        return generateBadge(metric, formatPercent(getNonNullValue(measure, LiveMeasureDto::getValue)), Color.DEFAULT);
+      case LEVEL:
+        return generateQualityGate(metric, measure);
+      case WORK_DUR:
+        return generateBadge(metric, formatDuration(getNonNullValue(measure, LiveMeasureDto::getValue).longValue()), Color.DEFAULT);
+      case RATING:
+        return generateRating(metric, measure);
+      default:
+        throw new IllegalStateException(format("Invalid metric type '%s'", metricType));
+    }
+  }
+
+  private String generateQualityGate(MetricDto metric, LiveMeasureDto measure) {
+    Level qualityGate = Level.valueOf(getNonNullValue(measure, LiveMeasureDto::getTextValue));
+    return generateBadge(metric, QUALITY_GATE_MESSAGE_BY_STATUS.get(qualityGate), COLOR_BY_QUALITY_GATE_STATUS.get(qualityGate));
+  }
+
+  private String generateRating(MetricDto metric, LiveMeasureDto measure) {
+    Rating rating = valueOf(getNonNullValue(measure, LiveMeasureDto::getValue).intValue());
+    return generateBadge(metric, rating.name(), COLOR_BY_RATING.get(rating));
+  }
+
+  private String generateBadge(MetricDto metric, String value, Color color) {
+    return svgGenerator.generateBadge(METRIC_NAME_BY_KEY.get(metric.getKey()), value, color);
+  }
+
+  private static <PARAM> PARAM getNonNullValue(LiveMeasureDto measure, Function<LiveMeasureDto, PARAM> function) {
+    PARAM value = function.apply(measure);
+    checkState(value != null, "Measure not found");
+    return value;
   }
 
 }
diff --git a/server/sonar-server/src/main/java/org/sonar/server/badge/ws/ProjectBadgesException.java b/server/sonar-server/src/main/java/org/sonar/server/badge/ws/ProjectBadgesException.java
new file mode 100644 (file)
index 0000000..4520ebd
--- /dev/null
@@ -0,0 +1,28 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2018 SonarSource SA
+ * mailto:info 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.badge.ws;
+
+class ProjectBadgesException extends RuntimeException {
+
+  ProjectBadgesException(String message) {
+    super(message);
+  }
+
+}
index 085fdc36291e6462975c7d66670a3d8d906742c6..98ffb09603b51d34064f2fd9032ee2933f592355 100644 (file)
@@ -39,7 +39,7 @@ public class ProjectBadgesWsModule extends Module {
     add(
       ProjectBadgesWs.class,
       QualityGateAction.class,
-      MeasureAction.class
-      );
+      MeasureAction.class,
+      SvgGenerator.class);
   }
 }
diff --git a/server/sonar-server/src/main/java/org/sonar/server/badge/ws/SvgFormatter.java b/server/sonar-server/src/main/java/org/sonar/server/badge/ws/SvgFormatter.java
new file mode 100644 (file)
index 0000000..e75911d
--- /dev/null
@@ -0,0 +1,83 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2018 SonarSource SA
+ * mailto:info 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.badge.ws;
+
+import java.text.DecimalFormat;
+import java.text.NumberFormat;
+import java.util.Locale;
+
+import static java.lang.String.format;
+import static org.apache.commons.lang.StringUtils.trim;
+
+class SvgFormatter {
+
+  private static final String ZERO = "0";
+
+  private static final NumberFormat NUMERIC_FORMATTER = DecimalFormat.getInstance(Locale.ENGLISH);
+  private static final String NUMERIC_SUFFIX_LIST = " kmbt";
+  private static final String NUMERIC_REGEXP = "\\.[0-9]+";
+
+  private static final DecimalFormat PERCENT_FORMATTER = new DecimalFormat("#.#");
+
+  private static final String DURATION_MINUTES_FORMAT = "%smin";
+  private static final String DURATION_HOURS_FORMAT = "%sh";
+  private static final String DURATION_DAYS_FORMAT = "%sd";
+  private static final int DURATION_HOURS_IN_DAY = 8;
+  private static final double DURATION_ALMOST_ONE = 0.9;
+  private static final int DURATION_OF_ONE_HOUR_IN_MINUTES = 60;
+
+  private SvgFormatter() {
+    // Only static methods
+  }
+
+  static String formatNumeric(long value) {
+    if (value == 0) {
+      return ZERO;
+    }
+    NUMERIC_FORMATTER.setMaximumFractionDigits(1);
+    int power = (int) StrictMath.log10(value);
+    double valueToFormat = value / (Math.pow(10, Math.floorDiv(power, 3) * 3d));
+    String formattedNumber = NUMERIC_FORMATTER.format(valueToFormat);
+    formattedNumber = formattedNumber + NUMERIC_SUFFIX_LIST.charAt(power / 3);
+    return formattedNumber.length() > 4 ? trim(formattedNumber.replaceAll(NUMERIC_REGEXP, "")) : trim(formattedNumber);
+  }
+
+  static String formatPercent(double value) {
+    return PERCENT_FORMATTER.format(value) + "%";
+  }
+
+  static String formatDuration(long durationInMinutes) {
+    if (durationInMinutes == 0) {
+      return ZERO;
+    }
+    double days = (double) durationInMinutes / DURATION_HOURS_IN_DAY / DURATION_OF_ONE_HOUR_IN_MINUTES;
+    if (days > DURATION_ALMOST_ONE) {
+      return format(DURATION_DAYS_FORMAT, Math.round(days));
+    }
+    double remainingDuration = durationInMinutes - (Math.floor(days) * DURATION_HOURS_IN_DAY * DURATION_OF_ONE_HOUR_IN_MINUTES);
+    double hours = remainingDuration / DURATION_OF_ONE_HOUR_IN_MINUTES;
+    if (hours > DURATION_ALMOST_ONE) {
+      return format(DURATION_HOURS_FORMAT, Math.round(hours));
+    }
+    double minutes = remainingDuration - (Math.floor(hours) * DURATION_OF_ONE_HOUR_IN_MINUTES);
+    return format(DURATION_MINUTES_FORMAT, Math.round(minutes));
+  }
+
+}
diff --git a/server/sonar-server/src/main/java/org/sonar/server/badge/ws/SvgGenerator.java b/server/sonar-server/src/main/java/org/sonar/server/badge/ws/SvgGenerator.java
new file mode 100644 (file)
index 0000000..8bf78cd
--- /dev/null
@@ -0,0 +1,119 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2018 SonarSource SA
+ * mailto:info 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.badge.ws;
+
+import com.google.common.collect.ImmutableMap;
+import java.awt.Font;
+import java.awt.font.FontRenderContext;
+import java.awt.geom.AffineTransform;
+import java.io.IOException;
+import java.util.Map;
+import org.apache.commons.io.IOUtils;
+import org.apache.commons.lang.text.StrSubstitutor;
+import org.sonar.api.server.ServerSide;
+
+import static java.lang.String.valueOf;
+import static java.nio.charset.StandardCharsets.UTF_8;
+
+@ServerSide
+public class SvgGenerator {
+
+  private static final FontRenderContext FONT_RENDER_CONTEXT = new FontRenderContext(new AffineTransform(), true, true);
+  private static final Font FONT = new Font("Verdana", Font.PLAIN, 11);
+
+  private static final int MARGIN = 6;
+
+  private static final String PARAMETER_MARGIN = "margin";
+  private static final String PARAMETER_TOTAL_WIDTH = "totalWidth";
+  private static final String PARAMETER_LABEL_WIDTH = "labelWidth";
+  private static final String PARAMETER_LABEL_WIDTH_PLUS_MARGIN = "LabelWidthPlusMargin";
+  private static final String PARAMETER_VALUE_WIDTH = "valueWidth";
+  private static final String PARAMETER_COLOR = "color";
+  private static final String PARAMETER_LABEL = "label";
+  private static final String PARAMETER_VALUE = "value";
+
+  private final String errorTemplate;
+  private final String badgeTemplate;
+
+  public SvgGenerator() {
+    this.errorTemplate = readTemplate("error.svg");
+    this.badgeTemplate = readTemplate("badge.svg");
+  }
+
+  public String generateBadge(String label, String value, Color backgroundValueColor) {
+    int labelWidth = computeWidth(label);
+    int valueWidth = computeWidth(value);
+
+    Map<String, String> values = ImmutableMap.<String, String>builder()
+      .put(PARAMETER_MARGIN, valueOf(MARGIN))
+      .put(PARAMETER_TOTAL_WIDTH, valueOf(MARGIN * 4 + labelWidth + valueWidth))
+      .put(PARAMETER_LABEL_WIDTH, valueOf(MARGIN * 2 + labelWidth))
+      .put(PARAMETER_LABEL_WIDTH_PLUS_MARGIN, valueOf(MARGIN * 3 + labelWidth))
+      .put(PARAMETER_VALUE_WIDTH, valueOf(MARGIN * 2 + valueWidth))
+      .put(PARAMETER_COLOR, backgroundValueColor.getValue())
+      .put(PARAMETER_LABEL, label)
+      .put(PARAMETER_VALUE, value)
+      .build();
+    StrSubstitutor strSubstitutor = new StrSubstitutor(values);
+    return strSubstitutor.replace(badgeTemplate);
+  }
+
+  public String generateError(String error) {
+    Map<String, String> values = ImmutableMap.of(
+      PARAMETER_TOTAL_WIDTH, valueOf(MARGIN + computeWidth(error) + MARGIN),
+      PARAMETER_LABEL, error);
+    StrSubstitutor strSubstitutor = new StrSubstitutor(values);
+    return strSubstitutor.replace(errorTemplate);
+  }
+
+  private static int computeWidth(String text) {
+    return (int) FONT.getStringBounds(text, FONT_RENDER_CONTEXT).getWidth();
+  }
+
+  private String readTemplate(String template) {
+    try {
+      return IOUtils.toString(getClass().getResource("templates/" + template), UTF_8);
+    } catch (IOException e) {
+      throw new IllegalStateException(String.format("Can't read svg template '%s'", template), e);
+    }
+  }
+
+  static class Color {
+    static final Color DEFAULT = new Color("#999");
+    static final Color QUALITY_GATE_OK = new Color("#4c1");
+    static final Color QUALITY_GATE_WARN = new Color("#ed7d20");
+    static final Color QUALITY_GATE_ERROR = new Color("#d4333f");
+    static final Color RATING_A = new Color("#00aa00");
+    static final Color RATING_B = new Color("#b0d513");
+    static final Color RATING_C = new Color("#eabe06");
+    static final Color RATING_D = new Color("#ed7d20");
+    static final Color RATING_E = new Color("#e00");
+
+    private final String value;
+
+    private Color(String value) {
+      this.value = value;
+    }
+
+    String getValue() {
+      return value;
+    }
+  }
+}
index 8934ac3cfc2485c23d9c892ff31048c68a97c3ca..3c1c4f97ecf39005db4d8ff4e809491a09cb4b27 100644 (file)
@@ -1,22 +1,20 @@
-<?xml version="1.0" encoding="UTF-8"?>
-<svg xmlns="http://www.w3.org/2000/svg" height="20" width="191">
-    <linearGradient id="smooth" x2="0" y2="100%">
+<svg xmlns="http://www.w3.org/2000/svg" height="20" width="127">
+    <linearGradient id="b" x2="0" y2="100%">
         <stop offset="0" stop-color="#bbb" stop-opacity=".1"/>
         <stop offset="1" stop-opacity=".1"/>
     </linearGradient>
-    <mask id="round">
-        <rect fill="#fff" height="20" rx="3" width="191"/>
-    </mask>
-    <g mask="url(#round)">
-        <rect fill="#666" height="20" width="136"/>
-        <rect fill="#969696" height="20" width="55" x="136"/>
-        <rect fill="url(#smooth)" height="20" width="191"/>
+    <clipPath id="a">
+        <rect width="127" height="20" rx="3" fill="#fff"/>
+    </clipPath>
+    <g clip-path="url(#a)">
+        <rect fill="#555" height="20" width="77"/>
+        <rect fill="#4c1" height="20" width="50" x="77"/>
+        <rect fill="url(#b)" height="20" width="127"/>
     </g>
-    <g fill="#fff" font-family="DejaVu Sans,Verdana,Sans PT,Lucida Grande,Tahoma,Helvetica,Arial,sans-serif"
-       font-size="11" text-anchor="middle">
-        <text fill="#010101" fill-opacity=".3" x="68" y="15">Coverage</text>
-        <text x="68" y="14">Coverage</text>
-        <text fill="#010101" fill-opacity=".3" x="163" y="15">100.0%</text>
-        <text x="163" y="14">100.0%</text>
+    <g fill="#fff" font-family="DejaVu Sans,Verdana,Geneva,sans-serif" font-size="11" text-anchor="left">
+        <text x="6" y="15" fill="#010101" fill-opacity=".3">quality gate</text>
+        <text x="6" y="14">quality gate</text>
+        <text x="83" y="15" fill="#010101" fill-opacity=".3">passed</text>
+        <text x="83" y="14">passed</text>
     </g>
 </svg>
\ No newline at end of file
index 038231f8b52dfc5974c9ad842a8873ebafbcb55f..06cba7000938036826121d05be724bb5331f3f50 100644 (file)
-<?xml version="1.0" encoding="UTF-8"?>
-<svg xmlns="http://www.w3.org/2000/svg" height="20" width="123">
-    <linearGradient id="smooth" x2="0" y2="100%">
-        <stop offset="0" stop-color="#bbb" stop-opacity=".1"/>
-        <stop offset="1" stop-opacity=".1"/>
-    </linearGradient>
-    <mask id="round">
-        <rect fill="#fff" height="20" rx="3" width="123"/>
-    </mask>
-    <g mask="url(#round)">
-        <rect fill="#666" height="20" width="77"/>
-        <rect fill="#E05D44" height="20" width="46" x="77"/>
-        <rect fill="url(#smooth)" height="20" width="123"/>
-    </g>
-    <g fill="#fff" font-family="Sans PT,Lucida Grande,Tahoma,Helvetica,Arial,sans-serif" font-size="11"
-       text-anchor="middle">
-        <text fill="#010101" fill-opacity=".3" x="38" y="15">Quality Gate</text>
-        <text x="38" y="14">Quality Gate</text>
-        <text fill="#010101" fill-opacity=".3" x="100" y="15">failing</text>
-        <text x="100" y="14">failing</text>
-    </g>
-</svg>
\ No newline at end of file
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Generator: Adobe Illustrator 22.0.1, SVG Export Plug-In . SVG Version: 6.00 Build 0)  -->
+<svg version="1.1" id="Calque_1" xmlns="http://www.w3.org/2000/svg"  x="0px" y="0px"
+        viewBox="0 0 350 262.5" style="enable-background:new 0 0 350 262.5;" xml:space="preserve">
+<style type="text/css">
+       .st0{fill:#FFFFFF;}
+       .st1{fill:#CFD3D7;}
+       .st2{fill:#29BE4C;}
+       .st3{fill:#F3702A;}
+       .st4{fill:#1B171B;}
+       .st5{enable-background:new    ;}
+       .st6{fill:#434447;}
+</style>
+<g>
+       <path class="st0" d="M328.4,259.5H21.5C10.9,259.5,2,250.8,2,240V21.9C2,11.3,10.7,2.4,21.5,2.4h307.1c10.6,0,19.5,8.7,19.5,19.5
+               V240C347.9,250.8,339.3,259.5,328.4,259.5z"/>
+       <path class="st1" d="M328.4,260.4H21.5c-11.2,0-20.4-9.2-20.4-20.4V21.9c0-11.2,9-20.4,20.4-20.4h307.1c11.2,0,20.4,9.2,20.4,20.4
+               V240C348.8,251.2,339.8,260.4,328.4,260.4z M21.5,3.3c-10.3,0-18.6,8.3-18.6,18.6V240c0,10.3,8.3,18.6,18.6,18.6h307.1
+               c10.3,0,18.6-8.3,18.6-18.6V21.9c0-10.3-8.3-18.6-18.6-18.6C328.6,3.3,21.5,3.3,21.5,3.3z"/>
+</g>
+<path class="st2" d="M234.3,162.9H115.5c-17.6,0-31.9-14.4-31.9-31.9l0,0c0-17.6,14.4-31.9,31.9-31.9h118.8
+       c17.6,0,31.9,14.4,31.9,31.9l0,0C266.2,148.5,251.8,162.9,234.3,162.9z"/>
+<g id="SonarCloud_Black_2_">
+       <path class="st3" d="M302.5,204.3c-1.6-1.9-3.7-3.3-6-4v-0.1c0-6.9-5.6-12.5-12.5-12.5s-12.5,5.6-12.5,12.5c0,0.1,0,0.1,0,0.2
+               c-5.1,1.6-8.8,6.3-8.8,11.9c0,6.9,5.6,12.5,12.5,12.5c3.3,0,6.5-1.3,8.8-3.6c2.3,2.2,5.4,3.6,8.8,3.6c6.9,0,12.5-5.6,12.5-12.5
+               C305.3,209.4,304.3,206.5,302.5,204.3z M292.8,221.6c-5.2,0-9.4-4.2-9.4-9.4c0-0.9-0.7-1.6-1.6-1.6s-1.6,0.7-1.6,1.6
+               c0,2.3,0.6,4.5,1.8,6.4c-1.8,1.9-4.2,3-6.8,3c-5.2,0-9.4-4.2-9.4-9.4s4.2-9.4,9.4-9.4c1.1,0,2.2,0.2,3.2,0.6l0,0
+               c0.4,0.1,0.9,0.4,1,0.5c0.7,0.6,1.7,0.5,2.2-0.2c0.6-0.7,0.5-1.7-0.2-2.2c-0.7-0.6-1.8-1-2-1.1c-1.4-0.5-2.8-0.8-4.3-0.8
+               c-0.2,0-0.4,0-0.6,0c0.2-5,4.3-9,9.3-9c5.2,0,9.4,4.2,9.4,9.4c0,3-1.5,5.9-3.9,7.6c-0.7,0.5-0.9,1.5-0.4,2.2
+               c0.3,0.4,0.8,0.7,1.3,0.7c0.3,0,0.6-0.1,0.9-0.3c2.4-1.7,4-4.1,4.8-6.9c3.6,1.3,6.1,4.8,6.1,8.8C302.2,217.4,298,221.6,292.8,221.6
+               z"/>
+       <g>
+               <path class="st4" d="M45.1,216c1.3,0.8,4,1.7,6,1.7c2.1,0,3-0.7,3-1.9s-0.7-1.7-3.3-2.6c-4.7-1.6-6.5-4.1-6.4-6.8
+                       c0-4.2,3.6-7.4,9.2-7.4c2.6,0,5,0.6,6.4,1.3l-1.2,4.8c-1-0.6-3-1.3-4.9-1.3c-1.7,0-2.7,0.7-2.7,1.8s0.9,1.6,3.6,2.6
+                       c4.3,1.5,6.1,3.6,6.1,7c0,4.2-3.3,7.3-9.8,7.3c-3,0-5.6-0.6-7.3-1.6L45.1,216z"/>
+               <path class="st4" d="M85.6,210.6c0,8.3-5.9,12-11.9,12c-6.6,0-11.7-4.3-11.7-11.6s4.8-11.9,12-11.9
+                       C81,199.1,85.6,203.8,85.6,210.6z M69.3,210.8c0,3.9,1.6,6.8,4.6,6.8c2.7,0,4.5-2.7,4.5-6.8c0-3.4-1.3-6.8-4.5-6.8
+                       C70.5,204.1,69.3,207.5,69.3,210.8z"/>
+               <path class="st4" d="M88.1,206.8c0-2.8-0.1-5.2-0.2-7.2H94l0.3,3.1h0.1c0.9-1.4,3.2-3.6,7-3.6c4.6,0,8.1,3,8.1,9.7v13.4h-7v-12.5
+                       c0-2.9-1-4.9-3.6-4.9c-1.9,0-3.1,1.3-3.5,2.6c-0.2,0.4-0.3,1.1-0.3,1.8v13h-7V206.8z"/>
+               <path class="st4" d="M126.1,222.1l-0.4-2.3h-0.1c-1.5,1.8-3.8,2.8-6.5,2.8c-4.6,0-7.3-3.3-7.3-6.9c0-5.9,5.3-8.7,13.2-8.6v-0.3
+                       c0-1.2-0.6-2.9-4.1-2.9c-2.3,0-4.7,0.8-6.2,1.7l-1.3-4.5c1.6-0.9,4.7-2,8.8-2c7.5,0,9.9,4.4,9.9,9.7v7.8c0,2.2,0.1,4.2,0.3,5.5
+                       H126.1z M125.3,211.5c-3.7,0-6.6,0.8-6.6,3.6c0,1.8,1.2,2.7,2.8,2.7c1.8,0,3.2-1.2,3.6-2.6c0.1-0.4,0.1-0.8,0.1-1.2L125.3,211.5
+                       L125.3,211.5z"/>
+               <path class="st4" d="M135.8,207c0-3.3-0.1-5.5-0.2-7.4h6l0.2,4.1h0.2c1.2-3.3,3.9-4.7,6.1-4.7c0.6,0,1,0,1.5,0.1v6.6
+                       c-0.5-0.1-1.1-0.2-1.9-0.2c-2.6,0-4.3,1.4-4.8,3.6c-0.1,0.5-0.1,1-0.1,1.6v11.4h-7V207z"/>
+               <path class="st4" d="M167.9,221.3c-1.1,0.6-3.4,1.3-6.4,1.3c-6.7,0-11.1-4.6-11.1-11.4c0-6.9,4.7-11.9,12-11.9
+                       c2.4,0,4.5,0.6,5.6,1.2l-0.9,3.1c-1-0.6-2.5-1.1-4.7-1.1c-5.1,0-7.9,3.8-7.9,8.4c0,5.2,3.3,8.3,7.7,8.3c2.3,0,3.8-0.6,5-1.1
+                       L167.9,221.3z"/>
+               <path class="st4" d="M170.9,189.4h4.1v32.7h-4.1V189.4z"/>
+               <path class="st4" d="M200.1,210.8c0,8.3-5.7,11.9-11.1,11.9c-6,0-10.7-4.4-10.7-11.5c0-7.5,4.9-11.9,11.1-11.9
+                       C195.8,199.3,200.1,204,200.1,210.8z M182.4,211c0,4.9,2.8,8.6,6.8,8.6c3.9,0,6.8-3.6,6.8-8.7c0-3.8-1.9-8.6-6.7-8.6
+                       C184.5,202.4,182.4,206.8,182.4,211z"/>
+               <path class="st4" d="M222.1,216c0,2.3,0,4.3,0.2,6.1h-3.6l-0.2-3.6h-0.1c-1.1,1.8-3.4,4.2-7.4,4.2c-3.5,0-7.7-1.9-7.7-9.8v-13.1
+                       h4.1v12.4c0,4.2,1.3,7.1,5,7.1c2.7,0,4.6-1.9,5.3-3.7c0.2-0.6,0.4-1.3,0.4-2.1v-13.7h4.1V216H222.1z"/>
+               <path class="st4" d="M246.2,189.4v27c0,2,0,4.2,0.2,5.8h-3.6l-0.2-3.9h-0.1c-1.2,2.5-4,4.4-7.6,4.4c-5.4,0-9.5-4.6-9.5-11.3
+                       c0-7.4,4.6-12,10-12c3.4,0,5.7,1.6,6.7,3.4h0.1v-13.3L246.2,189.4L246.2,189.4z M242.2,208.9c0-0.5,0-1.2-0.2-1.7
+                       c-0.6-2.6-2.8-4.7-5.9-4.7c-4.2,0-6.7,3.7-6.7,8.6c0,4.5,2.2,8.3,6.6,8.3c2.7,0,5.2-1.8,5.9-4.8c0.1-0.6,0.2-1.1,0.2-1.8
+                       L242.2,208.9L242.2,208.9z"/>
+       </g>
+</g>
+<g class="st5">
+       <path class="st6" d="M94.9,54.1c0,2.7-0.4,5-1.3,7c-0.9,1.9-2.1,3.5-3.6,4.6l5,3.9l-2.5,2.3l-5.9-4.7c-0.9,0.2-1.9,0.3-2.9,0.3
+               c-2.2,0-4.1-0.5-5.8-1.6c-1.7-1.1-3-2.6-3.9-4.6c-0.9-2-1.4-4.3-1.4-6.9v-2c0-2.7,0.5-5,1.4-7.1c0.9-2.1,2.2-3.6,3.9-4.7
+               s3.6-1.6,5.8-1.6c2.2,0,4.2,0.5,5.9,1.6c1.7,1.1,3,2.6,3.9,4.7c0.9,2,1.4,4.4,1.4,7.1V54.1z M91.3,52.3c0-3.3-0.7-5.8-2-7.6
+               c-1.3-1.8-3.2-2.7-5.6-2.7c-2.3,0-4.1,0.9-5.5,2.6c-1.3,1.8-2,4.2-2.1,7.4v2c0,3.2,0.7,5.7,2,7.5c1.3,1.8,3.2,2.8,5.6,2.8
+               s4.2-0.9,5.5-2.6c1.3-1.7,2-4.2,2-7.4V52.3z"/>
+       <path class="st6" d="M112.8,65.1c-1.4,1.6-3.4,2.4-6.1,2.4c-2.2,0-3.9-0.6-5-1.9s-1.7-3.2-1.7-5.7V46.5h3.5v13.4
+               c0,3.1,1.3,4.7,3.8,4.7c2.7,0,4.5-1,5.4-3v-15h3.5v20.7h-3.4L112.8,65.1z"/>
+       <path class="st6" d="M134.4,67.1c-0.2-0.4-0.4-1.1-0.5-2.2c-1.6,1.7-3.6,2.6-5.9,2.6c-2,0-3.7-0.6-5-1.7s-2-2.6-2-4.4
+               c0-2.2,0.8-3.8,2.5-5s3.9-1.8,6.9-1.8h3.4V53c0-1.2-0.4-2.2-1.1-3c-0.7-0.7-1.8-1.1-3.3-1.1c-1.3,0-2.3,0.3-3.2,1
+               c-0.9,0.6-1.3,1.4-1.3,2.3h-3.6c0-1,0.4-2,1.1-3c0.7-1,1.7-1.7,3-2.3c1.3-0.6,2.6-0.8,4.1-0.8c2.4,0,4.3,0.6,5.6,1.8
+               c1.4,1.2,2.1,2.8,2.1,4.9v9.5c0,1.9,0.2,3.4,0.7,4.5v0.3H134.4z M128.5,64.4c1.1,0,2.2-0.3,3.2-0.9c1-0.6,1.7-1.3,2.2-2.2v-4.2
+               h-2.8c-4.3,0-6.5,1.3-6.5,3.8c0,1.1,0.4,2,1.1,2.6C126.4,64.1,127.4,64.4,128.5,64.4z"/>
+       <path class="st6" d="M146.8,67.1h-3.5V37.8h3.5V67.1z"/>
+       <path class="st6" d="M152.5,41c0-0.6,0.2-1.1,0.5-1.5s0.9-0.6,1.6-0.6s1.2,0.2,1.6,0.6s0.5,0.9,0.5,1.5s-0.2,1.1-0.5,1.4
+               s-0.9,0.6-1.6,0.6s-1.2-0.2-1.6-0.6S152.5,41.5,152.5,41z M156.3,67.1h-3.5V46.5h3.5V67.1z"/>
+       <path class="st6" d="M166.7,41.4v5h3.9v2.7h-3.9V62c0,0.8,0.2,1.5,0.5,1.9c0.3,0.4,0.9,0.6,1.8,0.6c0.4,0,1-0.1,1.7-0.2v2.9
+               c-0.9,0.3-1.8,0.4-2.7,0.4c-1.6,0-2.8-0.5-3.6-1.4c-0.8-1-1.2-2.3-1.2-4.1V49.2h-3.8v-2.7h3.8v-5H166.7z"/>
+       <path class="st6" d="M181.5,62l4.8-15.5h3.8l-8.3,23.9c-1.3,3.4-3.3,5.2-6.1,5.2l-0.7-0.1l-1.3-0.2v-2.9l1,0.1
+               c1.2,0,2.1-0.2,2.8-0.7s1.2-1.4,1.7-2.7l0.8-2.1l-7.4-20.5h3.9L181.5,62z"/>
+       <path class="st6" d="M224.1,63.5c-0.9,1.4-2.3,2.4-3.9,3c-1.7,0.7-3.7,1-5.9,1c-2.3,0-4.3-0.5-6-1.6s-3.1-2.6-4.1-4.5
+               c-1-2-1.5-4.2-1.5-6.8v-2.4c0-4.2,1-7.4,2.9-9.8c2-2.3,4.7-3.5,8.2-3.5c2.9,0,5.2,0.7,7,2.2c1.8,1.5,2.9,3.6,3.2,6.3h-3.7
+               c-0.7-3.7-2.9-5.5-6.6-5.5c-2.5,0-4.3,0.9-5.6,2.6c-1.3,1.7-1.9,4.2-1.9,7.5v2.3c0,3.1,0.7,5.6,2.1,7.5c1.4,1.8,3.4,2.8,5.8,2.8
+               c1.4,0,2.6-0.2,3.6-0.5s1.9-0.8,2.6-1.5v-6.2H214v-3h10.1V63.5z"/>
+       <path class="st6" d="M242.4,67.1c-0.2-0.4-0.4-1.1-0.5-2.2c-1.6,1.7-3.6,2.6-5.9,2.6c-2,0-3.7-0.6-5-1.7s-2-2.6-2-4.4
+               c0-2.2,0.8-3.8,2.5-5s3.9-1.8,6.9-1.8h3.4V53c0-1.2-0.4-2.2-1.1-3c-0.7-0.7-1.8-1.1-3.3-1.1c-1.3,0-2.3,0.3-3.2,1
+               c-0.9,0.6-1.3,1.4-1.3,2.3h-3.6c0-1,0.4-2,1.1-3c0.7-1,1.7-1.7,3-2.3c1.3-0.6,2.6-0.8,4.1-0.8c2.4,0,4.3,0.6,5.6,1.8
+               c1.4,1.2,2.1,2.8,2.1,4.9v9.5c0,1.9,0.2,3.4,0.7,4.5v0.3H242.4z M236.5,64.4c1.1,0,2.2-0.3,3.2-0.9c1-0.6,1.7-1.3,2.2-2.2v-4.2
+               h-2.8c-4.3,0-6.5,1.3-6.5,3.8c0,1.1,0.4,2,1.1,2.6C234.4,64.1,235.4,64.4,236.5,64.4z"/>
+       <path class="st6" d="M255.7,41.4v5h3.9v2.7h-3.9V62c0,0.8,0.2,1.5,0.5,1.9c0.3,0.4,0.9,0.6,1.8,0.6c0.4,0,1-0.1,1.7-0.2v2.9
+               c-0.9,0.3-1.8,0.4-2.7,0.4c-1.6,0-2.8-0.5-3.6-1.4c-0.8-1-1.2-2.3-1.2-4.1V49.2h-3.8v-2.7h3.8v-5H255.7z"/>
+       <path class="st6" d="M272.3,67.5c-2.8,0-5.1-0.9-6.8-2.8c-1.8-1.8-2.6-4.3-2.6-7.4v-0.7c0-2.1,0.4-3.9,1.2-5.5
+               c0.8-1.6,1.9-2.9,3.3-3.8c1.4-0.9,2.9-1.4,4.6-1.4c2.7,0,4.8,0.9,6.3,2.7c1.5,1.8,2.2,4.3,2.2,7.6v1.5h-14c0.1,2,0.6,3.7,1.8,4.9
+               c1.1,1.3,2.6,1.9,4.3,1.9c1.2,0,2.3-0.3,3.2-0.8c0.9-0.5,1.6-1.2,2.3-2l2.2,1.7C278.4,66.2,275.8,67.5,272.3,67.5z M271.9,49
+               c-1.4,0-2.6,0.5-3.6,1.6c-1,1-1.6,2.5-1.8,4.4h10.4v-0.3c-0.1-1.8-0.6-3.2-1.5-4.2S273.3,49,271.9,49z"/>
+</g>
+<g class="st5">
+       <path class="st0" d="M131.5,133.8v10.7h-6.2v-30.3H137c2.3,0,4.3,0.4,6,1.2s3.1,2,4,3.6s1.4,3.3,1.4,5.2c0,3-1,5.3-3.1,7
+               c-2,1.7-4.8,2.6-8.4,2.6H131.5z M131.5,128.8h5.6c1.7,0,2.9-0.4,3.8-1.2c0.9-0.8,1.3-1.9,1.3-3.3c0-1.5-0.4-2.7-1.3-3.6
+               c-0.9-0.9-2.1-1.4-3.6-1.4h-5.7V128.8z"/>
+       <path class="st0" d="M166.1,138.3h-11l-2.1,6.2h-6.6l11.3-30.3h5.8l11.3,30.3h-6.6L166.1,138.3z M156.9,133.2h7.6l-3.8-11.3
+               L156.9,133.2z"/>
+       <path class="st0" d="M193.4,136.6c0-1.2-0.4-2.1-1.2-2.7s-2.3-1.3-4.5-2s-3.9-1.4-5.1-2.1c-3.4-1.9-5.2-4.4-5.2-7.5
+               c0-1.6,0.5-3.1,1.4-4.4c0.9-1.3,2.2-2.3,4-3s3.7-1.1,5.8-1.1c2.2,0,4.1,0.4,5.8,1.2s3,1.9,3.9,3.3c0.9,1.4,1.4,3.1,1.4,4.9h-6.2
+               c0-1.4-0.4-2.5-1.3-3.2c-0.9-0.8-2.1-1.2-3.7-1.2c-1.5,0-2.7,0.3-3.6,1c-0.8,0.6-1.3,1.5-1.3,2.6c0,1,0.5,1.8,1.5,2.5
+               c1,0.7,2.5,1.3,4.4,1.9c3.6,1.1,6.1,2.4,7.8,4c1.6,1.6,2.4,3.6,2.4,5.9c0,2.6-1,4.7-3,6.2s-4.7,2.2-8,2.2c-2.3,0-4.5-0.4-6.4-1.3
+               c-1.9-0.9-3.4-2-4.4-3.5c-1-1.5-1.5-3.2-1.5-5.2h6.3c0,3.3,2,5,6,5c1.5,0,2.6-0.3,3.5-0.9C193,138.5,193.4,137.7,193.4,136.6z"/>
+       <path class="st0" d="M219.7,136.6c0-1.2-0.4-2.1-1.2-2.7s-2.3-1.3-4.5-2s-3.9-1.4-5.1-2.1c-3.4-1.9-5.2-4.4-5.2-7.5
+               c0-1.6,0.5-3.1,1.4-4.4c0.9-1.3,2.2-2.3,4-3s3.7-1.1,5.8-1.1c2.2,0,4.1,0.4,5.8,1.2s3,1.9,3.9,3.3c0.9,1.4,1.4,3.1,1.4,4.9h-6.2
+               c0-1.4-0.4-2.5-1.3-3.2c-0.9-0.8-2.1-1.2-3.7-1.2c-1.5,0-2.7,0.3-3.6,1c-0.8,0.6-1.3,1.5-1.3,2.6c0,1,0.5,1.8,1.5,2.5
+               c1,0.7,2.5,1.3,4.4,1.9c3.6,1.1,6.1,2.4,7.8,4c1.6,1.6,2.4,3.6,2.4,5.9c0,2.6-1,4.7-3,6.2s-4.7,2.2-8,2.2c-2.3,0-4.5-0.4-6.4-1.3
+               c-1.9-0.9-3.4-2-4.4-3.5c-1-1.5-1.5-3.2-1.5-5.2h6.3c0,3.3,2,5,6,5c1.5,0,2.6-0.3,3.5-0.9C219.2,138.5,219.7,137.7,219.7,136.6z"/>
+</g>
+</svg>
diff --git a/server/sonar-server/src/main/resources/org/sonar/server/badge/ws/templates/badge.svg b/server/sonar-server/src/main/resources/org/sonar/server/badge/ws/templates/badge.svg
new file mode 100644 (file)
index 0000000..672699b
--- /dev/null
@@ -0,0 +1,20 @@
+<svg xmlns="http://www.w3.org/2000/svg" height="20" width="${totalWidth}">
+    <linearGradient id="b" x2="0" y2="100%">
+        <stop offset="0" stop-color="#bbb" stop-opacity=".1"/>
+        <stop offset="1" stop-opacity=".1"/>
+    </linearGradient>
+    <clipPath id="a">
+        <rect width="${totalWidth}" height="20" rx="3" fill="#fff"/>
+    </clipPath>
+    <g clip-path="url(#a)">
+        <rect fill="#555" height="20" width="${labelWidth}"/>
+        <rect fill="${color}" height="20" width="${valueWidth}" x="${labelWidth}"/>
+        <rect fill="url(#b)" height="20" width="${totalWidth}"/>
+    </g>
+    <g fill="#fff" font-family="DejaVu Sans,Verdana,Geneva,sans-serif" font-size="11" text-anchor="left">
+        <text x="${margin}" y="15" fill="#010101" fill-opacity=".3">${label}</text>
+        <text x="${margin}" y="14">${label}</text>
+        <text x="${LabelWidthPlusMargin}" y="15" fill="#010101" fill-opacity=".3">${value}</text>
+        <text x="${LabelWidthPlusMargin}" y="14">${value}</text>
+    </g>
+</svg>
\ No newline at end of file
diff --git a/server/sonar-server/src/main/resources/org/sonar/server/badge/ws/templates/error.svg b/server/sonar-server/src/main/resources/org/sonar/server/badge/ws/templates/error.svg
new file mode 100644 (file)
index 0000000..c9a7d35
--- /dev/null
@@ -0,0 +1,17 @@
+<svg xmlns="http://www.w3.org/2000/svg" height="20" width="${totalWidth}">
+    <linearGradient id="b" x2="0" y2="100%">
+        <stop offset="0" stop-color="#bbb" stop-opacity=".1"/>
+        <stop offset="1" stop-opacity=".1"/>
+    </linearGradient>
+    <clipPath id="a">
+        <rect width="${totalWidth}" height="20" rx="3" fill="#fff"/>
+    </clipPath>
+    <g clip-path="url(#a)">
+        <rect fill="#e05d44" height="20" width="${totalWidth}"/>
+        <rect fill="url(#b)" height="20" width="${totalWidth}"/>
+    </g>
+    <g fill="#fff" font-family="DejaVu Sans,Verdana,Geneva,sans-serif" font-size="11" text-anchor="left">
+        <text x="4" y="15" fill="#010101" fill-opacity=".3">${label}</text>
+        <text x="4" y="14">${label}</text>
+    </g>
+</svg>
\ No newline at end of file
index 51b619e5f9241e1ddfb2f6b08867e3d59bda7626..0f7a82d7c52af29b3bab201378ce642f34020342 100644 (file)
  */
 package org.sonar.server.badge.ws;
 
+import com.tngtech.java.junit.dataprovider.DataProvider;
+import com.tngtech.java.junit.dataprovider.DataProviderRunner;
+import com.tngtech.java.junit.dataprovider.UseDataProvider;
+import org.junit.Rule;
 import org.junit.Test;
+import org.junit.rules.ExpectedException;
+import org.junit.runner.RunWith;
+import org.sonar.api.measures.CoreMetrics;
+import org.sonar.api.measures.Metric.Level;
 import org.sonar.api.server.ws.WebService;
 import org.sonar.api.server.ws.WebService.Param;
+import org.sonar.api.web.UserRole;
+import org.sonar.db.DbTester;
+import org.sonar.db.component.BranchType;
+import org.sonar.db.component.ComponentDto;
+import org.sonar.db.metric.MetricDto;
+import org.sonar.server.badge.ws.SvgGenerator.Color;
+import org.sonar.server.component.ComponentFinder;
+import org.sonar.server.computation.task.projectanalysis.qualitymodel.Rating;
+import org.sonar.server.tester.UserSessionRule;
 import org.sonar.server.ws.WsActionTester;
 
 import static org.assertj.core.api.Assertions.assertThat;
 import static org.assertj.core.api.Assertions.tuple;
+import static org.sonar.api.measures.CoreMetrics.BUGS_KEY;
+import static org.sonar.api.measures.CoreMetrics.COVERAGE_KEY;
+import static org.sonar.api.measures.CoreMetrics.SQALE_RATING_KEY;
+import static org.sonar.api.measures.CoreMetrics.TECHNICAL_DEBT_KEY;
+import static org.sonar.api.measures.Metric.Level.ERROR;
+import static org.sonar.api.measures.Metric.Level.OK;
+import static org.sonar.api.measures.Metric.Level.WARN;
+import static org.sonar.api.measures.Metric.ValueType.INT;
+import static org.sonar.api.measures.Metric.ValueType.LEVEL;
+import static org.sonar.api.measures.Metric.ValueType.PERCENT;
+import static org.sonar.api.measures.Metric.ValueType.RATING;
+import static org.sonar.api.measures.Metric.ValueType.WORK_DUR;
+import static org.sonar.server.badge.ws.SvgGenerator.Color.DEFAULT;
+import static org.sonar.server.badge.ws.SvgGenerator.Color.QUALITY_GATE_ERROR;
+import static org.sonar.server.badge.ws.SvgGenerator.Color.QUALITY_GATE_OK;
+import static org.sonar.server.badge.ws.SvgGenerator.Color.QUALITY_GATE_WARN;
 
+@RunWith(DataProviderRunner.class)
 public class MeasureActionTest {
 
-  private WsActionTester ws = new WsActionTester(new MeasureAction());
+  @Rule
+  public ExpectedException expectedException = ExpectedException.none();
+  @Rule
+  public UserSessionRule userSession = UserSessionRule.standalone();
+  @Rule
+  public DbTester db = DbTester.create();
+
+  private WsActionTester ws = new WsActionTester(
+    new MeasureAction(userSession, db.getDbClient(), new ComponentFinder(db.getDbClient(), null), new SvgGenerator()));
+
+  @Test
+  public void int_measure() {
+    ComponentDto project = db.components().insertPublicProject();
+    userSession.registerComponents(project);
+    MetricDto metric = db.measures().insertMetric(m -> m.setKey(BUGS_KEY).setValueType(INT.name()));
+    db.measures().insertLiveMeasure(project, metric, m -> m.setValue(10_000d));
+
+    String response = ws.newRequest()
+      .setParam("project", project.getKey())
+      .setParam("metric", metric.getKey())
+      .execute().getInput();
+
+    checkSvg(response, "bugs", "10k", DEFAULT);
+  }
+
+  @Test
+  public void percent_measure() {
+    ComponentDto project = db.components().insertPublicProject();
+    userSession.registerComponents(project);
+    MetricDto metric = db.measures().insertMetric(m -> m.setKey(COVERAGE_KEY).setValueType(PERCENT.name()));
+    db.measures().insertLiveMeasure(project, metric, m -> m.setValue(12.345d));
+
+    String response = ws.newRequest()
+      .setParam("project", project.getKey())
+      .setParam("metric", metric.getKey())
+      .execute().getInput();
+
+    checkSvg(response, "coverage", "12.3%", DEFAULT);
+  }
+
+  @Test
+  public void duration_measure() {
+    ComponentDto project = db.components().insertPublicProject();
+    userSession.registerComponents(project);
+    MetricDto metric = db.measures().insertMetric(m -> m.setKey(TECHNICAL_DEBT_KEY).setValueType(WORK_DUR.name()));
+    db.measures().insertLiveMeasure(project, metric, m -> m.setValue(10_000d));
+
+    String response = ws.newRequest()
+      .setParam("project", project.getKey())
+      .setParam("metric", metric.getKey())
+      .execute().getInput();
+
+    checkSvg(response, "technical debt", "21d", DEFAULT);
+  }
+
+  @DataProvider
+  public static Object[][] ratings() {
+    return new Object[][] {
+      {Rating.A, Color.RATING_A},
+      {Rating.B, Color.RATING_B},
+      {Rating.C, Color.RATING_C},
+      {Rating.D, Color.RATING_D},
+      {Rating.E, Color.RATING_E}
+    };
+  }
+
+  @Test
+  @UseDataProvider("ratings")
+  public void rating_measure(Rating rating, Color color) {
+    ComponentDto project = db.components().insertPublicProject();
+    userSession.registerComponents(project);
+    MetricDto metric = db.measures().insertMetric(m -> m.setKey(SQALE_RATING_KEY).setValueType(RATING.name()));
+    db.measures().insertLiveMeasure(project, metric, m -> m.setValue((double) rating.getIndex()).setData(rating.name()));
+
+    String response = ws.newRequest()
+      .setParam("project", project.getKey())
+      .setParam("metric", metric.getKey())
+      .execute().getInput();
+
+    checkSvg(response, "maintainability", rating.name(), color);
+  }
+
+  @DataProvider
+  public static Object[][] qualityGates() {
+    return new Object[][] {
+      {OK, "passed", QUALITY_GATE_OK},
+      {WARN, "warning", QUALITY_GATE_WARN},
+      {ERROR, "failed", QUALITY_GATE_ERROR}
+    };
+  }
+
+  @Test
+  @UseDataProvider("qualityGates")
+  public void quality_gate(Level status, String expectedValue, Color expectedColor) {
+    ComponentDto project = db.components().insertPublicProject();
+    userSession.registerComponents(project);
+    MetricDto metric = createQualityGateMetric();
+    db.measures().insertLiveMeasure(project, metric, m -> m.setData(status.name()));
+
+    String response = ws.newRequest()
+      .setParam("project", project.getKey())
+      .setParam("metric", metric.getKey())
+      .execute().getInput();
+
+    checkSvg(response, "quality gate", expectedValue, expectedColor);
+  }
+
+  @Test
+  public void fail_on_invalid_quality_gate() {
+    ComponentDto project = db.components().insertMainBranch();
+    userSession.addProjectPermission(UserRole.USER, project);
+    MetricDto metric = createQualityGateMetric();
+    db.measures().insertLiveMeasure(project, metric, m -> m.setData("UNKNOWN"));
+
+    expectedException.expect(IllegalArgumentException.class);
+    expectedException.expectMessage("No enum constant org.sonar.api.measures.Metric.Level.UNKNOWN");
+
+    ws.newRequest()
+      .setParam("project", project.getKey())
+      .setParam("metric", metric.getKey())
+      .execute();
+  }
+
+  @Test
+  public void fail_when_measure_value_is_null() {
+    ComponentDto project = db.components().insertPublicProject();
+    userSession.registerComponents(project);
+    MetricDto metric = db.measures().insertMetric(m -> m.setKey(BUGS_KEY).setValueType(INT.name()));
+    db.measures().insertLiveMeasure(project, metric, m -> m.setValue(null));
+
+    expectedException.expect(IllegalStateException.class);
+    expectedException.expectMessage("Measure not found");
+
+    ws.newRequest()
+      .setParam("project", project.getKey())
+      .setParam("metric", metric.getKey())
+      .execute();
+  }
+
+  @Test
+  public void project_does_not_exist() {
+    MetricDto metric = db.measures().insertMetric(m -> m.setKey(BUGS_KEY));
+
+    String response = ws.newRequest()
+      .setParam("project", "unknown")
+      .setParam("metric", metric.getKey())
+      .execute().getInput();
+
+    checkError(response, "Component key 'unknown' not found");
+  }
+
+  @Test
+  public void branch_does_not_exist() {
+    ComponentDto project = db.components().insertMainBranch();
+    ComponentDto branch = db.components().insertProjectBranch(project, b -> b.setBranchType(BranchType.LONG));
+    userSession.addProjectPermission(UserRole.USER, project);
+    MetricDto metric = db.measures().insertMetric(m -> m.setKey(BUGS_KEY));
+    db.measures().insertLiveMeasure(project, metric, m -> m.setValue(10d));
+
+    String response = ws.newRequest()
+      .setParam("project", branch.getKey())
+      .setParam("branch", "unknown")
+      .setParam("metric", metric.getKey())
+      .execute().getInput();
+
+    checkError(response, String.format("Component '%s' on branch 'unknown' not found", branch.getKey()));
+  }
+
+  @Test
+  public void measure_not_found() {
+    ComponentDto project = db.components().insertPublicProject();
+    userSession.registerComponents(project);
+    MetricDto metric = db.measures().insertMetric(m -> m.setKey(BUGS_KEY));
+
+    String response = ws.newRequest()
+      .setParam("project", project.getKey())
+      .setParam("metric", metric.getKey())
+      .execute().getInput();
+
+    checkError(response, String.format("Measure '%s' has not been found for project '%s' and branch 'null'", metric.getKey(), project.getKey()));
+  }
+
+  @Test
+  public void unauthorized() {
+    ComponentDto project = db.components().insertPrivateProject();
+    MetricDto metric = db.measures().insertMetric(m -> m.setKey(BUGS_KEY));
+
+    String response = ws.newRequest()
+      .setParam("project", project.getKey())
+      .setParam("metric", metric.getKey())
+      .execute().getInput();
+
+    checkError(response, "Insufficient privileges");
+  }
+
+  @Test
+  public void fail_when_metric_not_found() {
+    ComponentDto project = db.components().insertMainBranch();
+    userSession.addProjectPermission(UserRole.USER, project);
+
+    expectedException.expect(IllegalStateException.class);
+    expectedException.expectMessage("Metric 'bugs' hasn't been found");
+
+    ws.newRequest()
+      .setParam("project", project.getKey())
+      .setParam("metric", BUGS_KEY)
+      .execute();
+  }
 
   @Test
   public void test_definition() {
@@ -37,20 +278,29 @@ public class MeasureActionTest {
     assertThat(def.key()).isEqualTo("measure");
     assertThat(def.isInternal()).isFalse();
     assertThat(def.isPost()).isFalse();
-    assertThat(def.since()).isNull();
+    assertThat(def.since()).isEqualTo("7.1");
     assertThat(def.responseExampleAsString()).isNotEmpty();
 
     assertThat(def.params())
       .extracting(Param::key, Param::isRequired)
       .containsExactlyInAnyOrder(
-        tuple("component", true),
+        tuple("project", true),
+        tuple("branch", false),
         tuple("metric", true));
   }
 
-  @Test
-  public void test_example() {
-    String response = ws.newRequest().execute().getInput();
+  private void checkSvg(String svg, String expectedLabel, String expectedValue, Color expectedColorValue) {
+    assertThat(svg).contains(
+      "<text", expectedLabel + "</text>",
+      "<text", expectedValue + "</text>",
+      "rect fill=\"" + expectedColorValue.getValue() + "\"");
+  }
+
+  private void checkError(String svg, String expectedError) {
+    assertThat(svg).contains("<text", ">" + expectedError + "</text>");
+  }
 
-    assertThat(response).isEqualTo(ws.getDef().responseExampleAsString());
+  private MetricDto createQualityGateMetric() {
+    return db.measures().insertMetric(m -> m.setKey(CoreMetrics.ALERT_STATUS_KEY).setValueType(LEVEL.name()));
   }
 }
index 69574191e116aa9f34a46d81fd27f2fd06c61c8f..0b6f717b2c0a07adcbc1e9ca92bc66ded7efb7d3 100644 (file)
@@ -38,7 +38,7 @@ public class ProjectBadgesWsModuleTest {
 
     underTest.configure(container);
 
-    assertThat(container.size()).isEqualTo(COMPONENTS_IN_EMPTY_COMPONENT_CONTAINER + 3);
+    assertThat(container.size()).isEqualTo(COMPONENTS_IN_EMPTY_COMPONENT_CONTAINER + 4);
   }
 
   @Test
diff --git a/server/sonar-server/src/test/java/org/sonar/server/badge/ws/SvgFormatterTest.java b/server/sonar-server/src/test/java/org/sonar/server/badge/ws/SvgFormatterTest.java
new file mode 100644 (file)
index 0000000..16b9b37
--- /dev/null
@@ -0,0 +1,101 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2018 SonarSource SA
+ * mailto:info 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.badge.ws;
+
+import org.junit.Test;
+import org.sonar.test.TestUtils;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.sonar.server.badge.ws.SvgFormatter.formatDuration;
+import static org.sonar.server.badge.ws.SvgFormatter.formatNumeric;
+import static org.sonar.server.badge.ws.SvgFormatter.formatPercent;
+
+public class SvgFormatterTest {
+
+  private static final int HOURS_IN_DAY = 8;
+
+  private static final long ONE_MINUTE = 1L;
+  private static final long ONE_HOUR = ONE_MINUTE * 60;
+  private static final long ONE_DAY = HOURS_IN_DAY * ONE_HOUR;
+
+  @Test
+  public void format_numeric() {
+    assertThat(formatNumeric(0L)).isEqualTo("0");
+
+    assertThat(formatNumeric(5L)).isEqualTo("5");
+    assertThat(formatNumeric(950L)).isEqualTo("950");
+
+    assertThat(formatNumeric(1_000L)).isEqualTo("1k");
+    assertThat(formatNumeric(1_010L)).isEqualTo("1k");
+    assertThat(formatNumeric(1_100L)).isEqualTo("1.1k");
+    assertThat(formatNumeric(1_690L)).isEqualTo("1.7k");
+    assertThat(formatNumeric(950_000L)).isEqualTo("950k");
+
+    assertThat(formatNumeric(1_000_000L)).isEqualTo("1m");
+    assertThat(formatNumeric(1_010_000L)).isEqualTo("1m");
+
+    assertThat(formatNumeric(1_000_000_000L)).isEqualTo("1b");
+
+    assertThat(formatNumeric(1_000_000_000_000L)).isEqualTo("1t");
+  }
+
+  @Test
+  public void format_percent() {
+    assertThat(formatPercent(0d)).isEqualTo("0%");
+    assertThat(formatPercent(12.345)).isEqualTo("12.3%");
+    assertThat(formatPercent(12.56)).isEqualTo("12.6%");
+  }
+
+  @Test
+  public void format_duration() {
+    assertThat(formatDuration(0)).isEqualTo("0");
+    assertThat(formatDuration(ONE_DAY)).isEqualTo("1d");
+    assertThat(formatDuration(ONE_HOUR)).isEqualTo("1h");
+    assertThat(formatDuration(ONE_MINUTE)).isEqualTo("1min");
+
+    assertThat(formatDuration(5 * ONE_DAY)).isEqualTo("5d");
+    assertThat(formatDuration(2 * ONE_HOUR)).isEqualTo("2h");
+    assertThat(formatDuration(ONE_MINUTE)).isEqualTo("1min");
+
+    assertThat(formatDuration(5 * ONE_DAY + 3 * ONE_HOUR)).isEqualTo("5d");
+    assertThat(formatDuration(3 * ONE_HOUR + 25 * ONE_MINUTE)).isEqualTo("3h");
+    assertThat(formatDuration(5 * ONE_DAY + 3 * ONE_HOUR + 40 * ONE_MINUTE)).isEqualTo("5d");
+  }
+
+  @Test
+  public void format_duration_is_rounding_result() {
+    // When starting to add more than 4 hours, the result will be rounded to the next day (as 4 hour is a half day)
+    assertThat(formatDuration(5 * ONE_DAY + 4 * ONE_HOUR)).isEqualTo("6d");
+    assertThat(formatDuration(5 * ONE_DAY + 5 * ONE_HOUR)).isEqualTo("6d");
+
+    // When starting to add more than 30 minutes, the result will be rounded to the next hour
+    assertThat(formatDuration(3 * ONE_HOUR + 30 * ONE_MINUTE)).isEqualTo("4h");
+    assertThat(formatDuration(3 * ONE_HOUR + 40 * ONE_MINUTE)).isEqualTo("4h");
+
+    // When duration is close to next unit (0.9), the result is rounded to next unit
+    assertThat(formatDuration(7 * ONE_HOUR + 20 + ONE_MINUTE)).isEqualTo("1d");
+    assertThat(formatDuration(55 * ONE_MINUTE)).isEqualTo("1h");
+  }
+
+  @Test
+  public void only_statics() {
+    assertThat(TestUtils.hasOnlyPrivateConstructors(SvgFormatter.class)).isTrue();
+  }
+}
index f69e2162ee8da5c51bfb9db244c56b9ae5cf8a7d..800f84477d965816a092c265102f48ccb2de93ef 100644 (file)
@@ -38,6 +38,7 @@ public final class MediaTypes {
   public static final String JAVASCRIPT = "application/javascript";
   public static final String HTML = "text/html";
   public static final String DEFAULT = "application/octet-stream";
+  public static final String SVG = "image/svg+xml";
 
   private static final Map<String, String> MAP = new ImmutableMap.Builder<String, String>()
     .put("js", JAVASCRIPT)
@@ -59,7 +60,7 @@ public final class MediaTypes {
     .put("jpeg", "image/jpeg")
     .put("tiff", "image/tiff")
     .put("png", "image/png")
-    .put("svg", "image/svg+xml")
+    .put("svg", SVG)
     .put("ico", "image/x-icon")
     .put("txt", TXT)
     .put("csv", "text/csv")