From b69f8ce778f5da7d703c228c6e946ad478e7e359 Mon Sep 17 00:00:00 2001 From: Julien Lancelot Date: Wed, 21 Mar 2018 10:17:37 +0100 Subject: [PATCH] SONAR-10412 Fix display of project badges --- .../sonar/server/badge/ws/MeasureAction.java | 21 ++- .../server/badge/ws/QualityGateAction.java | 18 ++- .../sonar/server/badge/ws/SvgGenerator.java | 102 ++++++++++++-- .../server/badge/ws/MeasureActionTest.java | 6 +- .../badge/ws/QualityGateActionTest.java | 4 +- .../server/badge/ws/SvgGeneratorTest.java | 124 ++++++++++++++++++ 6 files changed, 247 insertions(+), 28 deletions(-) create mode 100644 server/sonar-server/src/test/java/org/sonar/server/badge/ws/SvgGeneratorTest.java diff --git a/server/sonar-server/src/main/java/org/sonar/server/badge/ws/MeasureAction.java b/server/sonar-server/src/main/java/org/sonar/server/badge/ws/MeasureAction.java index 0fbac10bdb5..083e858ed3f 100644 --- a/server/sonar-server/src/main/java/org/sonar/server/badge/ws/MeasureAction.java +++ b/server/sonar-server/src/main/java/org/sonar/server/badge/ws/MeasureAction.java @@ -154,13 +154,9 @@ public class MeasureAction implements ProjectBadgesWsAction { @Override public void handle(Request request, Response response) throws Exception { response.stream().setMediaType(SVG); - String projectKey = request.mandatoryParam(PARAM_PROJECT); - String branch = request.param(PARAM_BRANCH); - String pullRequest = request.param(PARAM_PULL_REQUEST); String metricKey = request.mandatoryParam(PARAM_METRIC); try (DbSession dbSession = dbClient.openSession(false)) { - ComponentDto project = componentFinder.getByKeyAndOptionalBranchOrPullRequest(dbSession, projectKey, branch, pullRequest); - userSession.checkComponentPermission(USER, project); + ComponentDto project = getProject(dbSession, request); 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); @@ -170,9 +166,22 @@ public class MeasureAction implements ProjectBadgesWsAction { } } + private ComponentDto getProject(DbSession dbSession, Request request) { + try { + String projectKey = request.mandatoryParam(PARAM_PROJECT); + String branch = request.param(PARAM_BRANCH); + String pullRequest = request.param(PARAM_PULL_REQUEST); + ComponentDto project = componentFinder.getByKeyAndOptionalBranchOrPullRequest(dbSession, projectKey, branch, pullRequest); + userSession.checkComponentPermission(USER, project); + return project; + } catch (NotFoundException e) { + throw new NotFoundException("Component not found"); + } + } + 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()))); + .orElseThrow(() -> new ProjectBadgesException("Measure has not been found")); } private String generateSvg(MetricDto metric, LiveMeasureDto measure) { diff --git a/server/sonar-server/src/main/java/org/sonar/server/badge/ws/QualityGateAction.java b/server/sonar-server/src/main/java/org/sonar/server/badge/ws/QualityGateAction.java index bdf45ad78c6..066940e2c1f 100644 --- a/server/sonar-server/src/main/java/org/sonar/server/badge/ws/QualityGateAction.java +++ b/server/sonar-server/src/main/java/org/sonar/server/badge/ws/QualityGateAction.java @@ -87,11 +87,8 @@ public class QualityGateAction implements ProjectBadgesWsAction { @Override public void handle(Request request, Response response) throws Exception { response.stream().setMediaType(SVG); - String projectKey = request.mandatoryParam(PARAM_PROJECT); - String branch = request.param(PARAM_BRANCH); - String pullRequest = request.param(PARAM_PULL_REQUEST); try (DbSession dbSession = dbClient.openSession(false)) { - ComponentDto project = componentFinder.getByKeyAndOptionalBranchOrPullRequest(dbSession, projectKey, branch, pullRequest); + ComponentDto project = getProject(dbSession, request); userSession.checkComponentPermission(USER, project); Level qualityGateStatus = getQualityGate(dbSession, project); write(svgGenerator.generateQualityGate(qualityGateStatus), response.stream().output(), UTF_8); @@ -100,6 +97,19 @@ public class QualityGateAction implements ProjectBadgesWsAction { } } + private ComponentDto getProject(DbSession dbSession, Request request) { + try { + String projectKey = request.mandatoryParam(PARAM_PROJECT); + String branch = request.param(PARAM_BRANCH); + String pullRequest = request.param(PARAM_PULL_REQUEST); + ComponentDto project = componentFinder.getByKeyAndOptionalBranchOrPullRequest(dbSession, projectKey, branch, pullRequest); + userSession.checkComponentPermission(USER, project); + return project; + } catch (NotFoundException e) { + throw new NotFoundException("Component not found"); + } + } + private Level getQualityGate(DbSession dbSession, ComponentDto project) { return Level.valueOf(dbClient.liveMeasureDao().selectMeasure(dbSession, project.uuid(), CoreMetrics.ALERT_STATUS_KEY) .map(LiveMeasureDto::getTextValue) 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 index 0da1c8fa654..b486dab9298 100644 --- 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 @@ -20,9 +20,6 @@ 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; @@ -31,6 +28,7 @@ import org.sonar.api.config.Configuration; import org.sonar.api.measures.Metric; import org.sonar.api.server.ServerSide; +import static com.google.common.base.Preconditions.checkState; import static java.lang.String.valueOf; import static java.nio.charset.StandardCharsets.UTF_8; import static org.sonar.api.measures.Metric.Level.ERROR; @@ -41,19 +39,88 @@ import static org.sonar.process.ProcessProperties.Property.SONARCLOUD_ENABLED; @ServerSide public class SvgGenerator { + private static final Map CHAR_LENGTH = ImmutableMap.builder() + .put('0', 7) + .put('1', 7) + .put('2', 7) + .put('3', 7) + .put('4', 7) + .put('5', 7) + .put('6', 7) + .put('7', 7) + .put('8', 7) + .put('9', 7) + .put('a', 7) + .put('b', 7) + .put('c', 6) + .put('d', 7) + .put('e', 6) + .put('f', 4) + .put('g', 7) + .put('h', 7) + .put('i', 3) + .put('j', 5) + .put('k', 6) + .put('l', 3) + .put('m', 11) + .put('n', 7) + .put('o', 7) + .put('p', 7) + .put('q', 7) + .put('r', 5) + .put('s', 6) + .put('t', 4) + .put('u', 7) + .put('v', 6) + .put('w', 9) + .put('x', 6) + .put('y', 6) + .put('z', 6) + .put('A', 7) + .put('B', 7) + .put('C', 8) + .put('D', 8) + .put('E', 7) + .put('F', 6) + .put('G', 8) + .put('H', 8) + .put('I', 5) + .put('J', 5) + .put('K', 7) + .put('L', 6) + .put('M', 9) + .put('N', 8) + .put('O', 9) + .put('P', 7) + .put('Q', 9) + .put('R', 8) + .put('S', 7) + .put('T', 7) + .put('U', 8) + .put('V', 10) + .put('W', 10) + .put('X', 7) + .put('Y', 7) + .put('Z', 7) + .put('%', 12) + .put(' ', 4) + .put('.', 4) + .put('_', 7) + .put('\'', 3) + .build(); + private static final String TEMPLATES_SONARCLOUD = "templates/sonarcloud"; private static final String TEMPLATES_SONARQUBE = "templates/sonarqube"; - 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 int ICON_WIDTH = 20; private static final String PARAMETER_ICON_WIDTH_PLUS_MARGIN = "iconWidthPlusMargin"; private static final String PARAMETER_TOTAL_WIDTH = "totalWidth"; + private static final String PARAMETER_LEFT_WIDTH = "leftWidth"; + private static final String PARAMETER_LEFT_WIDTH_PLUS_MARGIN = "leftWidthPlusMargin"; + private static final String PARAMETER_RIGHT_WIDTH = "rightWidth"; 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"; @@ -79,11 +146,13 @@ public class SvgGenerator { int valueWidth = computeWidth(value); Map values = ImmutableMap.builder() - .put(PARAMETER_ICON_WIDTH_PLUS_MARGIN, valueOf(MARGIN + ICON_WIDTH)) .put(PARAMETER_TOTAL_WIDTH, valueOf(MARGIN * 4 + ICON_WIDTH + labelWidth + valueWidth)) - .put(PARAMETER_LABEL_WIDTH, valueOf(MARGIN * 2 + ICON_WIDTH + labelWidth)) - .put(PARAMETER_LABEL_WIDTH_PLUS_MARGIN, valueOf( MARGIN * 3 + ICON_WIDTH + labelWidth)) - .put(PARAMETER_VALUE_WIDTH, valueOf(MARGIN * 2 + valueWidth)) + .put(PARAMETER_LABEL_WIDTH, valueOf(labelWidth)) + .put(PARAMETER_VALUE_WIDTH, valueOf(valueWidth)) + .put(PARAMETER_LEFT_WIDTH, valueOf(MARGIN * 2 + ICON_WIDTH + labelWidth)) + .put(PARAMETER_LEFT_WIDTH_PLUS_MARGIN, valueOf(MARGIN * 3 + ICON_WIDTH + labelWidth)) + .put(PARAMETER_RIGHT_WIDTH, valueOf(MARGIN * 2 + valueWidth)) + .put(PARAMETER_ICON_WIDTH_PLUS_MARGIN, valueOf(MARGIN + ICON_WIDTH)) .put(PARAMETER_COLOR, backgroundValueColor.getValue()) .put(PARAMETER_LABEL, label) .put(PARAMETER_VALUE, value) @@ -92,7 +161,7 @@ public class SvgGenerator { return strSubstitutor.replace(badgeTemplate); } - public String generateQualityGate(Metric.Level level){ + public String generateQualityGate(Metric.Level level) { return qualityGateTemplates.get(level); } @@ -105,7 +174,14 @@ public class SvgGenerator { } private static int computeWidth(String text) { - return (int) FONT.getStringBounds(text, FONT_RENDER_CONTEXT).getWidth(); + return text.chars() + .mapToObj(i -> (char)i) + .mapToInt(c -> { + Integer length = CHAR_LENGTH.get(c); + checkState(length != null, "Invalid character '%s'", c); + return length; + }) + .sum(); } private String readTemplate(String template) { diff --git a/server/sonar-server/src/test/java/org/sonar/server/badge/ws/MeasureActionTest.java b/server/sonar-server/src/test/java/org/sonar/server/badge/ws/MeasureActionTest.java index 1f1f9e00f11..3691bbdfb87 100644 --- a/server/sonar-server/src/test/java/org/sonar/server/badge/ws/MeasureActionTest.java +++ b/server/sonar-server/src/test/java/org/sonar/server/badge/ws/MeasureActionTest.java @@ -214,7 +214,7 @@ public class MeasureActionTest { .setParam("metric", metric.getKey()) .execute().getInput(); - checkError(response, "Component key 'unknown' not found"); + checkError(response, "Component not found"); } @Test @@ -231,7 +231,7 @@ public class MeasureActionTest { .setParam("metric", metric.getKey()) .execute().getInput(); - checkError(response, String.format("Component '%s' on branch 'unknown' not found", branch.getKey())); + checkError(response, "Component not found"); } @Test @@ -245,7 +245,7 @@ public class MeasureActionTest { .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())); + checkError(response, "Measure has not been found"); } @Test diff --git a/server/sonar-server/src/test/java/org/sonar/server/badge/ws/QualityGateActionTest.java b/server/sonar-server/src/test/java/org/sonar/server/badge/ws/QualityGateActionTest.java index 5b9d0c94fec..1daf4020583 100644 --- a/server/sonar-server/src/test/java/org/sonar/server/badge/ws/QualityGateActionTest.java +++ b/server/sonar-server/src/test/java/org/sonar/server/badge/ws/QualityGateActionTest.java @@ -109,7 +109,7 @@ public class QualityGateActionTest { .setParam("project", "unknown") .execute().getInput(); - checkError(response, "Component key 'unknown' not found"); + checkError(response, "Component not found"); } @Test @@ -123,7 +123,7 @@ public class QualityGateActionTest { .setParam("branch", "unknown") .execute().getInput(); - checkError(response, format("Component '%s' on branch 'unknown' not found", branch.getKey())); + checkError(response, format("Component not found", branch.getKey())); } @Test diff --git a/server/sonar-server/src/test/java/org/sonar/server/badge/ws/SvgGeneratorTest.java b/server/sonar-server/src/test/java/org/sonar/server/badge/ws/SvgGeneratorTest.java new file mode 100644 index 00000000000..e7971334931 --- /dev/null +++ b/server/sonar-server/src/test/java/org/sonar/server/badge/ws/SvgGeneratorTest.java @@ -0,0 +1,124 @@ +/* + * SonarQube + * Copyright (C) 2009-2017 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.io.IOException; +import org.apache.commons.io.IOUtils; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.ExpectedException; +import org.sonar.api.config.internal.MapSettings; +import org.sonar.api.measures.Metric; +import org.sonar.db.DbTester; +import org.sonar.server.tester.UserSessionRule; + +import static java.nio.charset.StandardCharsets.UTF_8; +import static org.assertj.core.api.Assertions.assertThat; +import static org.sonar.api.measures.Metric.Level.WARN; +import static org.sonar.server.badge.ws.SvgGenerator.Color.DEFAULT; + +public class SvgGeneratorTest { + + @Rule + public ExpectedException expectedException = ExpectedException.none(); + @Rule + public UserSessionRule userSession = UserSessionRule.standalone(); + @Rule + public DbTester db = DbTester.create(); + + private MapSettings mapSettings = new MapSettings(); + + private SvgGenerator underTest; + + @Test + public void generate_badge() { + mapSettings.setProperty("sonar.sonarcloud.enabled", false); + initSvgGenerator(); + + String result = underTest.generateBadge("label", "10", DEFAULT); + + checkBadge(result, "label", "10", DEFAULT); + } + + @Test + public void generate_quality_gate() { + mapSettings.setProperty("sonar.sonarcloud.enabled", false); + initSvgGenerator(); + + String result = underTest.generateQualityGate(WARN); + + checkQualityGate(result, WARN); + } + + @Test + public void generate_error() { + mapSettings.setProperty("sonar.sonarcloud.enabled", false); + initSvgGenerator(); + + String result = underTest.generateError("Error"); + + assertThat(result).contains("Error"); + } + + @Test + public void fail_when_unknown_character() { + mapSettings.setProperty("sonar.sonarcloud.enabled", false); + initSvgGenerator(); + + expectedException.expectMessage("Invalid character 'é'"); + + underTest.generateError("Méssage with accent"); + } + + private void initSvgGenerator() { + underTest = new SvgGenerator(mapSettings.asConfig()); + } + + private void checkBadge(String svg, String expectedLabel, String expectedValue, SvgGenerator.Color expectedColorValue) { + assertThat(svg).contains( + "", + "", + "rect fill=\"" + expectedColorValue.getValue() + "\""); + } + + private void checkQualityGate(String response, Metric.Level status) { + switch (status) { + case OK: + assertThat(response).isEqualTo(readTemplate("quality_gate_passed.svg")); + break; + case WARN: + assertThat(response).isEqualTo(readTemplate("quality_gate_warn.svg")); + break; + case ERROR: + assertThat(response).isEqualTo(readTemplate("quality_gate_failed.svg")); + break; + } + } + + private String readTemplate(String template) { + try { + return IOUtils.toString(getClass().getResource("templates/sonarqube/" + template), UTF_8); + } catch (IOException e) { + throw new IllegalStateException(String.format("Can't read svg template '%s'", template), e); + } + } + +} -- 2.39.5