Bläddra i källkod

SONAR-10117 SONAR-1018 Update measures after relevant issue changes and send webhooks

tags/7.0-RC1
Simon Brandhof 6 år sedan
förälder
incheckning
baccb26f1a
100 ändrade filer med 2628 tillägg och 872 borttagningar
  1. 8
    2
      plugins/sonar-xoo-plugin/src/main/java/org/sonar/xoo/rule/OneBugIssuePerLineSensor.java
  2. 3
    0
      server/sonar-ce/src/main/java/org/sonar/ce/container/ComputeEngineContainerImpl.java
  3. 2
    1
      server/sonar-ce/src/test/java/org/sonar/ce/container/ComputeEngineContainerImplTest.java
  4. 7
    1
      server/sonar-db-dao/src/main/java/org/sonar/db/component/ComponentDto.java
  5. 4
    0
      server/sonar-db-dao/src/main/java/org/sonar/db/issue/IssueDao.java
  6. 98
    0
      server/sonar-db-dao/src/main/java/org/sonar/db/issue/IssueGroupDto.java
  7. 6
    0
      server/sonar-db-dao/src/main/java/org/sonar/db/issue/IssueMapper.java
  8. 4
    1
      server/sonar-db-dao/src/main/java/org/sonar/db/measure/LiveMeasureDao.java
  9. 1
    1
      server/sonar-db-dao/src/main/java/org/sonar/db/metric/MetricDao.java
  10. 1
    2
      server/sonar-db-dao/src/main/java/org/sonar/db/webhook/WebhookDeliveryDto.java
  11. 36
    0
      server/sonar-db-dao/src/main/resources/org/sonar/db/issue/IssueMapper.xml
  12. 20
    5
      server/sonar-db-dao/src/test/java/org/sonar/db/component/ComponentDtoTest.java
  13. 55
    0
      server/sonar-db-dao/src/test/java/org/sonar/db/issue/IssueDaoTest.java
  14. 133
    15
      server/sonar-db-dao/src/test/java/org/sonar/db/measure/LiveMeasureDaoTest.java
  15. 2
    2
      server/sonar-db-dao/src/test/java/org/sonar/db/purge/PurgeDaoTest.java
  16. 2
    1
      server/sonar-server/src/main/java/org/sonar/server/component/index/ComponentIndexer.java
  17. 1
    1
      server/sonar-server/src/main/java/org/sonar/server/component/ws/AppAction.java
  18. 1
    1
      server/sonar-server/src/main/java/org/sonar/server/computation/task/projectanalysis/api/posttask/PostProjectAnalysisTasksExecutor.java
  19. 1
    1
      server/sonar-server/src/main/java/org/sonar/server/computation/task/projectanalysis/formula/counter/RatingValue.java
  20. 6
    6
      server/sonar-server/src/main/java/org/sonar/server/computation/task/projectanalysis/qualitygate/MutableQualityGateHolder.java
  21. 9
    1
      server/sonar-server/src/main/java/org/sonar/server/computation/task/projectanalysis/qualitygate/QualityGateHolder.java
  22. 21
    22
      server/sonar-server/src/main/java/org/sonar/server/computation/task/projectanalysis/qualitygate/QualityGateHolderImpl.java
  23. 3
    2
      server/sonar-server/src/main/java/org/sonar/server/computation/task/projectanalysis/qualitygate/QualityGateService.java
  24. 3
    3
      server/sonar-server/src/main/java/org/sonar/server/computation/task/projectanalysis/qualitygate/QualityGateServiceImpl.java
  25. 20
    32
      server/sonar-server/src/main/java/org/sonar/server/computation/task/projectanalysis/qualitymodel/DebtRatingGrid.java
  26. 2
    5
      server/sonar-server/src/main/java/org/sonar/server/computation/task/projectanalysis/qualitymodel/MaintainabilityMeasuresVisitor.java
  27. 7
    4
      server/sonar-server/src/main/java/org/sonar/server/computation/task/projectanalysis/qualitymodel/NewMaintainabilityMeasuresVisitor.java
  28. 6
    6
      server/sonar-server/src/main/java/org/sonar/server/computation/task/projectanalysis/qualitymodel/NewReliabilityAndSecurityRatingMeasuresVisitor.java
  29. 63
    0
      server/sonar-server/src/main/java/org/sonar/server/computation/task/projectanalysis/qualitymodel/Rating.java
  30. 29
    38
      server/sonar-server/src/main/java/org/sonar/server/computation/task/projectanalysis/qualitymodel/RatingSettings.java
  31. 23
    55
      server/sonar-server/src/main/java/org/sonar/server/computation/task/projectanalysis/qualitymodel/ReliabilityAndSecurityRatingMeasuresVisitor.java
  32. 3
    7
      server/sonar-server/src/main/java/org/sonar/server/computation/task/projectanalysis/step/LoadQualityGateStep.java
  33. 2
    2
      server/sonar-server/src/main/java/org/sonar/server/computation/task/projectanalysis/step/QualityGateMeasuresStep.java
  34. 2
    1
      server/sonar-server/src/main/java/org/sonar/server/computation/task/projectanalysis/webhook/WebhookPostTask.java
  35. 2
    1
      server/sonar-server/src/main/java/org/sonar/server/es/ProjectIndexer.java
  36. 5
    0
      server/sonar-server/src/main/java/org/sonar/server/issue/AbstractChangeTagsAction.java
  37. 2
    0
      server/sonar-server/src/main/java/org/sonar/server/issue/Action.java
  38. 5
    0
      server/sonar-server/src/main/java/org/sonar/server/issue/AssignAction.java
  39. 5
    0
      server/sonar-server/src/main/java/org/sonar/server/issue/CommentAction.java
  40. 39
    0
      server/sonar-server/src/main/java/org/sonar/server/issue/IssueChangePostProcessor.java
  41. 46
    0
      server/sonar-server/src/main/java/org/sonar/server/issue/IssueChangePostProcessorImpl.java
  42. 26
    14
      server/sonar-server/src/main/java/org/sonar/server/issue/IssueUpdater.java
  43. 5
    0
      server/sonar-server/src/main/java/org/sonar/server/issue/SetSeverityAction.java
  44. 5
    0
      server/sonar-server/src/main/java/org/sonar/server/issue/SetTypeAction.java
  45. 5
    0
      server/sonar-server/src/main/java/org/sonar/server/issue/TransitionAction.java
  46. 2
    1
      server/sonar-server/src/main/java/org/sonar/server/issue/index/IssueIndexer.java
  47. 1
    1
      server/sonar-server/src/main/java/org/sonar/server/issue/ws/AddCommentAction.java
  48. 2
    2
      server/sonar-server/src/main/java/org/sonar/server/issue/ws/AssignAction.java
  49. 48
    64
      server/sonar-server/src/main/java/org/sonar/server/issue/ws/BulkChangeAction.java
  50. 2
    18
      server/sonar-server/src/main/java/org/sonar/server/issue/ws/DoTransitionAction.java
  51. 0
    4
      server/sonar-server/src/main/java/org/sonar/server/issue/ws/IssueWsModule.java
  52. 1
    1
      server/sonar-server/src/main/java/org/sonar/server/issue/ws/SetSeverityAction.java
  53. 1
    1
      server/sonar-server/src/main/java/org/sonar/server/issue/ws/SetTagsAction.java
  54. 2
    19
      server/sonar-server/src/main/java/org/sonar/server/issue/ws/SetTypeAction.java
  55. 14
    3
      server/sonar-server/src/main/java/org/sonar/server/measure/index/ProjectMeasuresIndexer.java
  56. 153
    0
      server/sonar-server/src/main/java/org/sonar/server/measure/live/IssueCounter.java
  57. 89
    0
      server/sonar-server/src/main/java/org/sonar/server/measure/live/IssueMetricFormula.java
  58. 40
    0
      server/sonar-server/src/main/java/org/sonar/server/measure/live/IssueMetricFormulaFactory.java
  59. 200
    0
      server/sonar-server/src/main/java/org/sonar/server/measure/live/IssueMetricFormulaFactoryImpl.java
  60. 43
    0
      server/sonar-server/src/main/java/org/sonar/server/measure/live/LiveMeasureComputer.java
  61. 242
    0
      server/sonar-server/src/main/java/org/sonar/server/measure/live/LiveMeasureComputerImpl.java
  62. 32
    0
      server/sonar-server/src/main/java/org/sonar/server/measure/live/LiveMeasureModule.java
  63. 40
    0
      server/sonar-server/src/main/java/org/sonar/server/measure/live/LiveQualityGateComputer.java
  64. 151
    0
      server/sonar-server/src/main/java/org/sonar/server/measure/live/LiveQualityGateComputerImpl.java
  65. 203
    0
      server/sonar-server/src/main/java/org/sonar/server/measure/live/MeasureMatrix.java
  66. 23
    0
      server/sonar-server/src/main/java/org/sonar/server/measure/live/package-info.java
  67. 1
    1
      server/sonar-server/src/main/java/org/sonar/server/measure/ws/ComponentAction.java
  68. 1
    1
      server/sonar-server/src/main/java/org/sonar/server/measure/ws/SearchAction.java
  69. 2
    2
      server/sonar-server/src/main/java/org/sonar/server/permission/index/PermissionIndexer.java
  70. 4
    0
      server/sonar-server/src/main/java/org/sonar/server/platform/platformlevel/PlatformLevel4.java
  71. 16
    3
      server/sonar-server/src/main/java/org/sonar/server/qualitygate/Condition.java
  72. 181
    0
      server/sonar-server/src/main/java/org/sonar/server/qualitygate/ConditionEvaluator.java
  73. 2
    1
      server/sonar-server/src/main/java/org/sonar/server/qualitygate/EvaluatedCondition.java
  74. 29
    27
      server/sonar-server/src/main/java/org/sonar/server/qualitygate/EvaluatedQualityGate.java
  75. 0
    134
      server/sonar-server/src/main/java/org/sonar/server/qualitygate/LiveQualityGateFactoryImpl.java
  76. 2
    2
      server/sonar-server/src/main/java/org/sonar/server/qualitygate/QualityGateConditionsUpdater.java
  77. 61
    0
      server/sonar-server/src/main/java/org/sonar/server/qualitygate/QualityGateConverter.java
  78. 60
    0
      server/sonar-server/src/main/java/org/sonar/server/qualitygate/QualityGateEvaluator.java
  79. 133
    0
      server/sonar-server/src/main/java/org/sonar/server/qualitygate/QualityGateEvaluatorImpl.java
  80. 6
    6
      server/sonar-server/src/main/java/org/sonar/server/qualitygate/QualityGateFinder.java
  81. 1
    0
      server/sonar-server/src/main/java/org/sonar/server/qualitygate/QualityGateModule.java
  82. 2
    2
      server/sonar-server/src/main/java/org/sonar/server/qualitygate/RegisterQualityGates.java
  83. 6
    0
      server/sonar-server/src/main/java/org/sonar/server/qualitygate/ShortLivingBranchQualityGate.java
  84. 0
    138
      server/sonar-server/src/main/java/org/sonar/server/qualitygate/changeevent/QGChangeEventFactory.java
  85. 0
    156
      server/sonar-server/src/main/java/org/sonar/server/qualitygate/changeevent/QGChangeEventFactoryImpl.java
  86. 3
    1
      server/sonar-server/src/main/java/org/sonar/server/qualitygate/changeevent/QGChangeEventListeners.java
  87. 4
    3
      server/sonar-server/src/main/java/org/sonar/server/qualitygate/changeevent/QGChangeEventListenersImpl.java
  88. 1
    1
      server/sonar-server/src/main/java/org/sonar/server/qualitygate/ws/GetByProjectAction.java
  89. 9
    0
      server/sonar-server/src/main/java/org/sonar/server/settings/ProjectConfigurationLoader.java
  90. 2
    3
      server/sonar-server/src/main/java/org/sonar/server/test/index/TestIndexer.java
  91. 1
    1
      server/sonar-server/src/main/java/org/sonar/server/ui/ws/ComponentAction.java
  92. 7
    4
      server/sonar-server/src/main/java/org/sonar/server/webhook/WebhookQGChangeEventListener.java
  93. 5
    10
      server/sonar-server/src/main/java/org/sonar/server/webhook/ws/WebhookWsSupport.java
  94. 5
    5
      server/sonar-server/src/test/java/org/sonar/server/computation/task/projectanalysis/formula/counter/RatingValueTest.java
  95. 2
    2
      server/sonar-server/src/test/java/org/sonar/server/computation/task/projectanalysis/measure/BestValueOptimizationTest.java
  96. 11
    5
      server/sonar-server/src/test/java/org/sonar/server/computation/task/projectanalysis/qualitygate/MutableQualityGateHolderRule.java
  97. 27
    6
      server/sonar-server/src/test/java/org/sonar/server/computation/task/projectanalysis/qualitygate/QualityGateHolderImplTest.java
  98. 17
    2
      server/sonar-server/src/test/java/org/sonar/server/computation/task/projectanalysis/qualitygate/QualityGateHolderRule.java
  99. 9
    9
      server/sonar-server/src/test/java/org/sonar/server/computation/task/projectanalysis/qualitymodel/DebtRatingGridTest.java
  100. 0
    0
      server/sonar-server/src/test/java/org/sonar/server/computation/task/projectanalysis/qualitymodel/MaintainabilityMeasuresVisitorTest.java

+ 8
- 2
plugins/sonar-xoo-plugin/src/main/java/org/sonar/xoo/rule/OneBugIssuePerLineSensor.java Visa fil

@@ -23,6 +23,7 @@ import org.sonar.api.batch.fs.FilePredicates;
import org.sonar.api.batch.fs.FileSystem;
import org.sonar.api.batch.fs.InputFile;
import org.sonar.api.batch.fs.InputFile.Type;
import org.sonar.api.batch.fs.TextRange;
import org.sonar.api.batch.sensor.Sensor;
import org.sonar.api.batch.sensor.SensorContext;
import org.sonar.api.batch.sensor.SensorDescriptor;
@@ -56,15 +57,20 @@ public class OneBugIssuePerLineSensor implements Sensor {
}
}

private void createIssues(InputFile file, SensorContext context, String repo) {
private static void createIssues(InputFile file, SensorContext context, String repo) {
RuleKey ruleKey = RuleKey.of(repo, RULE_KEY);
for (int line = 1; line <= file.lines(); line++) {
TextRange text = file.selectLine(line);
// do not count empty lines, which can be a pain with end-of-file return
if (text.end().lineOffset() == 0) {
continue;
}
NewIssue newIssue = context.newIssue();
newIssue
.forRule(ruleKey)
.at(newIssue.newLocation()
.on(file)
.at(file.selectLine(line))
.at(text)
.message("This bug issue is generated on each line"))
.save();
}

+ 3
- 0
server/sonar-ce/src/main/java/org/sonar/ce/container/ComputeEngineContainerImpl.java Visa fil

@@ -137,6 +137,7 @@ import org.sonar.server.plugins.ServerExtensionInstaller;
import org.sonar.server.plugins.privileged.PrivilegedPluginsBootstraper;
import org.sonar.server.plugins.privileged.PrivilegedPluginsStopper;
import org.sonar.server.property.InternalPropertiesImpl;
import org.sonar.server.qualitygate.QualityGateModule;
import org.sonar.server.qualityprofile.index.ActiveRuleIndexer;
import org.sonar.server.rule.CommonRuleDefinitionsImpl;
import org.sonar.server.rule.DefaultRuleFinder;
@@ -430,6 +431,8 @@ public class ComputeEngineContainerImpl implements ComputeEngineContainer {
// webhooks
WebhookModule.class,

QualityGateModule.class,

// cleaning
CeCleaningModule.class);


+ 2
- 1
server/sonar-ce/src/test/java/org/sonar/ce/container/ComputeEngineContainerImplTest.java Visa fil

@@ -91,7 +91,8 @@ public class ComputeEngineContainerImplTest {
assertThat(picoContainer.getComponentAdapters())
.hasSize(
CONTAINER_ITSELF
+ 77 // level 4
+ 78 // level 4
+ 21 // content of QualityGateModule
+ 6 // content of CeConfigurationModule
+ 4 // content of CeQueueModule
+ 4 // content of CeHttpModule

+ 7
- 1
server/sonar-db-dao/src/main/java/org/sonar/db/component/ComponentDto.java Visa fil

@@ -29,9 +29,11 @@ import javax.annotation.Nullable;
import org.apache.commons.lang.StringUtils;
import org.apache.commons.lang.builder.ToStringBuilder;
import org.sonar.api.resources.Scopes;
import org.sonar.db.WildcardPosition;

import static com.google.common.base.Preconditions.checkArgument;
import static java.lang.String.format;
import static org.sonar.db.DaoDatabaseUtils.buildLikeValue;
import static org.sonar.db.component.ComponentValidator.checkComponentKey;
import static org.sonar.db.component.ComponentValidator.checkComponentName;
import static org.sonar.db.component.DbTagsReader.readDbTags;
@@ -157,6 +159,10 @@ public class ComponentDto {
return parent.getUuidPath() + parent.uuid() + UUID_PATH_SEPARATOR;
}

public String getUuidPathLikeIncludingSelf() {
return buildLikeValue(formatUuidPathFromParent(this), WildcardPosition.AFTER);
}

public Long getId() {
return id;
}
@@ -196,7 +202,7 @@ public class ComponentDto {
/**
* List of ancestor UUIDs, ordered by depth in tree.
*/
List<String> getUuidPathAsList() {
public List<String> getUuidPathAsList() {
return UUID_PATH_SPLITTER.splitToList(uuidPath);
}


+ 4
- 0
server/sonar-db-dao/src/main/java/org/sonar/db/issue/IssueDao.java Visa fil

@@ -107,6 +107,10 @@ public class IssueDao implements Dao {
return executeLargeInputs(componentUuids, mapper(dbSession)::selectOpenByComponentUuids);
}

public Collection<IssueGroupDto> selectIssueGroupsByBaseComponent(DbSession dbSession, ComponentDto baseComponent, long leakPeriodBeginningDate) {
return mapper(dbSession).selectIssueGroupsByBaseComponent(baseComponent, leakPeriodBeginningDate);
}

public void insert(DbSession session, IssueDto dto) {
mapper(session).insert(dto);
}

+ 98
- 0
server/sonar-db-dao/src/main/java/org/sonar/db/issue/IssueGroupDto.java Visa fil

@@ -0,0 +1,98 @@
/*
* 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.db.issue;

import javax.annotation.CheckForNull;
import javax.annotation.Nullable;

public class IssueGroupDto {
private int ruleType;
private String severity;
@Nullable
private String resolution;
private String status;
private double effort;
private long count;
private boolean inLeak;

public int getRuleType() {
return ruleType;
}

public String getSeverity() {
return severity;
}

@CheckForNull
public String getResolution() {
return resolution;
}

public String getStatus() {
return status;
}

public double getEffort() {
return effort;
}

public long getCount() {
return count;
}

public boolean isInLeak() {
return inLeak;
}

public IssueGroupDto setRuleType(int ruleType) {
this.ruleType = ruleType;
return this;
}

public IssueGroupDto setSeverity(String severity) {
this.severity = severity;
return this;
}

public IssueGroupDto setResolution(@Nullable String resolution) {
this.resolution = resolution;
return this;
}

public IssueGroupDto setStatus(String status) {
this.status = status;
return this;
}

public IssueGroupDto setEffort(double effort) {
this.effort = effort;
return this;
}

public IssueGroupDto setCount(long count) {
this.count = count;
return this;
}

public IssueGroupDto setInLeak(boolean inLeak) {
this.inLeak = inLeak;
return this;
}
}

+ 6
- 0
server/sonar-db-dao/src/main/java/org/sonar/db/issue/IssueMapper.java Visa fil

@@ -19,10 +19,12 @@
*/
package org.sonar.db.issue;

import java.util.Collection;
import java.util.List;
import java.util.Set;
import org.apache.ibatis.annotations.Param;
import org.apache.ibatis.session.ResultHandler;
import org.sonar.db.component.ComponentDto;

public interface IssueMapper {

@@ -46,4 +48,8 @@ public interface IssueMapper {
@Param("projectUuid") String projectUuid,
@Param("likeModuleUuidPath") String likeModuleUuidPath,
ResultHandler<IssueDto> handler);

Collection<IssueGroupDto> selectIssueGroupsByBaseComponent(
@Param("baseComponent") ComponentDto baseComponent,
@Param("leakPeriodBeginningDate") long leakPeriodBeginningDate);
}

+ 4
- 1
server/sonar-db-dao/src/main/java/org/sonar/db/measure/LiveMeasureDao.java Visa fil

@@ -42,7 +42,7 @@ public class LiveMeasureDao implements Dao {
this.system2 = system2;
}

public List<LiveMeasureDto> selectByComponentUuids(DbSession dbSession, Collection<String> largeComponentUuids, Collection<Integer> metricIds) {
public List<LiveMeasureDto> selectByComponentUuidsAndMetricIds(DbSession dbSession, Collection<String> largeComponentUuids, Collection<Integer> metricIds) {
if (largeComponentUuids.isEmpty() || metricIds.isEmpty()) {
return Collections.emptyList();
}
@@ -90,6 +90,9 @@ public class LiveMeasureDao implements Dao {
}
}

/**
* Delete the rows that do NOT have the specified marker
*/
public void deleteByProjectUuidExcludingMarker(DbSession dbSession, String projectUuid, String marker) {
mapper(dbSession).deleteByProjectUuidExcludingMarker(projectUuid, marker);
}

+ 1
- 1
server/sonar-db-dao/src/main/java/org/sonar/db/metric/MetricDao.java Visa fil

@@ -47,7 +47,7 @@ public class MetricDao implements Dao {
return mapper(session).selectByKey(key);
}

public List<MetricDto> selectByKeys(final DbSession session, List<String> keys) {
public List<MetricDto> selectByKeys(final DbSession session, Collection<String> keys) {
return executeLargeInputs(keys, mapper(session)::selectByKeys);
}


+ 1
- 2
server/sonar-db-dao/src/main/java/org/sonar/db/webhook/WebhookDeliveryDto.java Visa fil

@@ -39,12 +39,11 @@ public class WebhookDeliveryDto extends WebhookDeliveryLiteDto<WebhookDeliveryDt
return this;
}

@CheckForNull
public String getPayload() {
return payload;
}

public WebhookDeliveryDto setPayload(@Nullable String s) {
public WebhookDeliveryDto setPayload(String s) {
this.payload = s;
return this;
}

+ 36
- 0
server/sonar-db-dao/src/main/resources/org/sonar/db/issue/IssueMapper.xml Visa fil

@@ -246,5 +246,41 @@
p.module_uuid_path like #{likeModuleUuidPath, jdbcType=VARCHAR} escape '/' and
i.status &lt;&gt; 'CLOSED'
</select>

<select id="selectIssueGroupsByBaseComponent" resultType="org.sonar.db.issue.IssueGroupDto" parameterType="map">
select i.issue_type as ruleType, i.severity as severity, i.resolution as resolution, i.status as status, sum(i.effort) as effort, count(i.issue_type) as "count", (i.issue_creation_date &gt;= #{leakPeriodBeginningDate,jdbcType=BIGINT}) as inLeak
from issues i
inner join projects p on p.uuid = i.component_uuid and p.project_uuid = i.project_uuid
where i.status !='CLOSED'
and i.project_uuid = #{baseComponent.projectUuid,jdbcType=VARCHAR}
and (p.uuid_path like #{baseComponent.uuidPathLikeIncludingSelf,jdbcType=VARCHAR} escape '/' or p.uuid = #{baseComponent.uuid,jdbcType=VARCHAR})
group by i.issue_type, i.severity, i.resolution, i.status, inLeak
</select>

<select id="selectIssueGroupsByBaseComponent" resultType="org.sonar.db.issue.IssueGroupDto" parameterType="map" databaseId="oracle">
select i2.issue_type as ruleType, i2.severity as severity, i2.resolution as resolution, i2.status as status, sum(i2.effort) as effort, count(i2.issue_type) as "count", i2.inLeak as inLeak
from (
select i.issue_type, i.severity, i.resolution, i.status, i.effort, case when i.issue_creation_date &gt; #{leakPeriodBeginningDate,jdbcType=BIGINT} then 1 else 0 end as inLeak
from issues i
inner join projects p on p.uuid = i.component_uuid and p.project_uuid = i.project_uuid
where i.status !='CLOSED'
and i.project_uuid = #{baseComponent.projectUuid,jdbcType=VARCHAR}
and (p.uuid_path like #{baseComponent.uuidPathLikeIncludingSelf,jdbcType=VARCHAR} escape '/' or p.uuid = #{baseComponent.uuid,jdbcType=VARCHAR})
) i2
group by i2.issue_type, i2.severity, i2.resolution, i2.status, i2.inLeak
</select>

<select id="selectIssueGroupsByBaseComponent" resultType="org.sonar.db.issue.IssueGroupDto" parameterType="map" databaseId="mssql">
select i2.issue_type as ruleType, i2.severity as severity, i2.resolution as resolution, i2.status as status, sum(i2.effort) as effort, count(i2.issue_type) as "count", i2.inLeak as inLeak
from (
select i.issue_type, i.severity, i.resolution, i.status, i.effort, case when i.issue_creation_date &gt; #{leakPeriodBeginningDate,jdbcType=BIGINT} then 1 else 0 end as inLeak
from issues i
inner join projects p on p.uuid = i.component_uuid and p.project_uuid = i.project_uuid
where i.status !='CLOSED'
and i.project_uuid = #{baseComponent.projectUuid,jdbcType=VARCHAR}
and (p.uuid_path like #{baseComponent.uuidPathLikeIncludingSelf,jdbcType=VARCHAR} escape '/' or p.uuid = #{baseComponent.uuid,jdbcType=VARCHAR})
) i2
group by i2.issue_type, i2.severity, i2.resolution, i2.status, i2.inLeak
</select>
</mapper>


+ 20
- 5
server/sonar-db-dao/src/test/java/org/sonar/db/component/ComponentDtoTest.java Visa fil

@@ -22,9 +22,10 @@ package org.sonar.db.component;
import org.junit.Test;
import org.sonar.api.resources.Qualifiers;
import org.sonar.api.resources.Scopes;
import org.sonar.db.organization.OrganizationTesting;
import org.sonar.db.organization.OrganizationDto;

import static org.assertj.core.api.Assertions.assertThat;
import static org.sonar.db.organization.OrganizationTesting.newOrganizationDto;

public class ComponentDtoTest {

@@ -89,13 +90,27 @@ public class ComponentDtoTest {
}

@Test
public void test_formatUuidPathFromParent() {
ComponentDto parent = ComponentTesting.newPrivateProjectDto(OrganizationTesting.newOrganizationDto(), "123").setUuidPath(ComponentDto.UUID_PATH_OF_ROOT);
public void formatUuidPathFromParent() {
ComponentDto parent = ComponentTesting.newPrivateProjectDto(newOrganizationDto(), "123").setUuidPath(ComponentDto.UUID_PATH_OF_ROOT);
assertThat(ComponentDto.formatUuidPathFromParent(parent)).isEqualTo(".123.");
}

@Test
public void test_Name() {
public void getUuidPathLikeIncludingSelf() {
OrganizationDto organizationDto = newOrganizationDto();

ComponentDto project = ComponentTesting.newPrivateProjectDto(organizationDto).setUuidPath(ComponentDto.UUID_PATH_OF_ROOT);
assertThat(project.getUuidPathLikeIncludingSelf()).isEqualTo("." + project.uuid() + ".%");

ComponentDto module = ComponentTesting.newModuleDto(project);
assertThat(module.getUuidPathLikeIncludingSelf()).isEqualTo("." + project.uuid() + "." + module.uuid() + ".%");

ComponentDto file = ComponentTesting.newFileDto(module);
assertThat(file.getUuidPathLikeIncludingSelf()).isEqualTo("." + project.uuid() + "." + module.uuid() + "." + file.uuid() + ".%");
}

@Test
public void getUuidPathAsList() {
ComponentDto root = new ComponentDto().setUuidPath(ComponentDto.UUID_PATH_OF_ROOT);
assertThat(root.getUuidPathAsList()).isEmpty();

@@ -104,7 +119,7 @@ public class ComponentDtoTest {
}

@Test
public void test_getKey_and_getBranch() {
public void getKey_and_getBranch() {
ComponentDto underTest = new ComponentDto().setDbKey("my_key:BRANCH:my_branch");
assertThat(underTest.getKey()).isEqualTo("my_key");
assertThat(underTest.getBranch()).isEqualTo("my_branch");

+ 55
- 0
server/sonar-db-dao/src/test/java/org/sonar/db/issue/IssueDaoTest.java Visa fil

@@ -21,6 +21,7 @@ package org.sonar.db.issue;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
import org.apache.ibatis.session.ResultContext;
@@ -30,6 +31,7 @@ import org.junit.Test;
import org.junit.rules.ExpectedException;
import org.sonar.api.issue.Issue;
import org.sonar.api.rule.RuleKey;
import org.sonar.api.rules.RuleType;
import org.sonar.api.utils.System2;
import org.sonar.db.DbTester;
import org.sonar.db.RowNotFoundException;
@@ -234,6 +236,59 @@ public class IssueDaoTest {
assertThat(fp.getIssueCreationDate()).isNotNull();
}

@Test
public void test_selectGroupsOfComponentTreeOnLeak_on_component_without_issues() {
ComponentDto project = db.components().insertPublicProject();
ComponentDto file = db.components().insertComponent(ComponentTesting.newFileDto(project));

Collection<IssueGroupDto> groups = underTest.selectIssueGroupsByBaseComponent(db.getSession(), file, 1_000L);

assertThat(groups).isEmpty();
}

@Test
public void selectGroupsOfComponentTreeOnLeak_on_file() {
ComponentDto project = db.components().insertPublicProject();
ComponentDto file = db.components().insertComponent(ComponentTesting.newFileDto(project));
RuleDefinitionDto rule = db.rules().insert();
IssueDto fpBug = db.issues().insert(rule, project, file,
i -> i.setStatus("RESOLVED").setResolution("FALSE-POSITIVE").setSeverity("MAJOR").setType(RuleType.BUG).setIssueCreationTime(1_500L));
IssueDto criticalBug1 = db.issues().insert(rule, project, file,
i -> i.setStatus("OPEN").setResolution(null).setSeverity("CRITICAL").setType(RuleType.BUG).setIssueCreationTime(1_600L));
IssueDto criticalBug2 = db.issues().insert(rule, project, file,
i -> i.setStatus("OPEN").setResolution(null).setSeverity("CRITICAL").setType(RuleType.BUG).setIssueCreationTime(1_700L));
// closed issues are ignored
IssueDto closed = db.issues().insert(rule, project, file,
i -> i.setStatus("CLOSED").setResolution("REMOVED").setSeverity("CRITICAL").setType(RuleType.BUG).setIssueCreationTime(1_700L));

Collection<IssueGroupDto> result = underTest.selectIssueGroupsByBaseComponent(db.getSession(), file, 1_000L);

assertThat(result.stream().mapToLong(IssueGroupDto::getCount).sum()).isEqualTo(3);

assertThat(result.stream().filter(g -> g.getRuleType()==RuleType.BUG.getDbConstant()).mapToLong(IssueGroupDto::getCount).sum()).isEqualTo(3);
assertThat(result.stream().filter(g -> g.getRuleType()==RuleType.CODE_SMELL.getDbConstant()).mapToLong(IssueGroupDto::getCount).sum()).isEqualTo(0);
assertThat(result.stream().filter(g -> g.getRuleType()==RuleType.VULNERABILITY.getDbConstant()).mapToLong(IssueGroupDto::getCount).sum()).isEqualTo(0);

assertThat(result.stream().filter(g -> g.getSeverity().equals("CRITICAL")).mapToLong(IssueGroupDto::getCount).sum()).isEqualTo(2);
assertThat(result.stream().filter(g -> g.getSeverity().equals("MAJOR")).mapToLong(IssueGroupDto::getCount).sum()).isEqualTo(1);
assertThat(result.stream().filter(g -> g.getSeverity().equals("MINOR")).mapToLong(IssueGroupDto::getCount).sum()).isEqualTo(0);

assertThat(result.stream().filter(g -> g.getStatus().equals("OPEN")).mapToLong(IssueGroupDto::getCount).sum()).isEqualTo(2);
assertThat(result.stream().filter(g -> g.getStatus().equals("RESOLVED")).mapToLong(IssueGroupDto::getCount).sum()).isEqualTo(1);
assertThat(result.stream().filter(g -> g.getStatus().equals("CLOSED")).mapToLong(IssueGroupDto::getCount).sum()).isEqualTo(0);

assertThat(result.stream().filter(g -> "FALSE-POSITIVE" .equals(g.getResolution())).mapToLong(IssueGroupDto::getCount).sum()).isEqualTo(1);
assertThat(result.stream().filter(g -> g.getResolution()== null).mapToLong(IssueGroupDto::getCount).sum()).isEqualTo(2);

assertThat(result.stream().filter(g -> g.isInLeak()).mapToLong(IssueGroupDto::getCount).sum()).isEqualTo(3);
assertThat(result.stream().filter(g -> !g.isInLeak()).mapToLong(IssueGroupDto::getCount).sum()).isEqualTo(0);

// test leak
result = underTest.selectIssueGroupsByBaseComponent(db.getSession(), file, 999_999_999L);
assertThat(result.stream().filter(g -> g.isInLeak()).mapToLong(IssueGroupDto::getCount).sum()).isEqualTo(0);
assertThat(result.stream().filter(g -> !g.isInLeak()).mapToLong(IssueGroupDto::getCount).sum()).isEqualTo(3);
}

private static IssueDto newIssueDto(String key) {
IssueDto dto = new IssueDto();
dto.setComponent(new ComponentDto().setDbKey("struts:Action").setId(123L).setUuid("component-uuid"));

+ 133
- 15
server/sonar-db-dao/src/test/java/org/sonar/db/measure/LiveMeasureDaoTest.java Visa fil

@@ -19,59 +19,115 @@
*/
package org.sonar.db.measure;

import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.List;
import org.assertj.core.groups.Tuple;
import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import org.sonar.api.utils.System2;
import org.sonar.db.DbTester;
import org.sonar.db.component.ComponentDto;
import org.sonar.db.metric.MetricDto;

import static java.util.Arrays.asList;
import static java.util.Collections.emptyList;
import static java.util.Collections.singleton;
import static java.util.Collections.singletonList;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.groups.Tuple.tuple;
import static org.sonar.db.component.ComponentTesting.newFileDto;
import static org.sonar.db.component.ComponentTesting.newPrivateProjectDto;
import static org.sonar.db.measure.MeasureTesting.newLiveMeasure;

public class LiveMeasureDaoTest {

private static final int A_METRIC_ID = 42;

@Rule
public DbTester db = DbTester.create(System2.INSTANCE);

private LiveMeasureDao underTest = db.getDbClient().liveMeasureDao();
private MetricDto metric;

@Before
public void setUp() throws Exception {
metric = db.measures().insertMetric();
}

@Test
public void test_selectByComponentUuids() {
LiveMeasureDto measure1 = newLiveMeasure().setMetricId(A_METRIC_ID);
LiveMeasureDto measure2 = newLiveMeasure().setMetricId(A_METRIC_ID);
public void test_selectByComponentUuidsAndMetricIds() {
LiveMeasureDto measure1 = newLiveMeasure().setMetricId(metric.getId());
LiveMeasureDto measure2 = newLiveMeasure().setMetricId(metric.getId());
underTest.insert(db.getSession(), measure1);
underTest.insert(db.getSession(), measure2);

List<LiveMeasureDto> selected = underTest.selectByComponentUuids(db.getSession(), asList(measure1.getComponentUuid(), measure2.getComponentUuid()), singletonList(A_METRIC_ID));
List<LiveMeasureDto> selected = underTest.selectByComponentUuidsAndMetricIds(db.getSession(),
asList(measure1.getComponentUuid(), measure2.getComponentUuid()), singletonList(metric.getId()));
assertThat(selected)
.extracting(LiveMeasureDto::getComponentUuid, LiveMeasureDto::getProjectUuid, LiveMeasureDto::getMetricId, LiveMeasureDto::getValue, LiveMeasureDto::getDataAsString)
.containsExactlyInAnyOrder(
Tuple.tuple(measure1.getComponentUuid(), measure1.getProjectUuid(), measure1.getMetricId(), measure1.getValue(), measure1.getDataAsString()),
Tuple.tuple(measure2.getComponentUuid(), measure2.getProjectUuid(), measure2.getMetricId(), measure2.getValue(), measure2.getDataAsString()));
tuple(measure1.getComponentUuid(), measure1.getProjectUuid(), measure1.getMetricId(), measure1.getValue(), measure1.getDataAsString()),
tuple(measure2.getComponentUuid(), measure2.getProjectUuid(), measure2.getMetricId(), measure2.getValue(), measure2.getDataAsString()));

assertThat(underTest.selectByComponentUuidsAndMetricIds(db.getSession(), emptyList(), singletonList(metric.getId()))).isEmpty();
assertThat(underTest.selectByComponentUuidsAndMetricIds(db.getSession(), singletonList(measure1.getComponentUuid()), emptyList())).isEmpty();
}

@Test
public void selectByComponentUuids_returns_empty_list_if_metric_does_not_match() {
LiveMeasureDto measure = newLiveMeasure().setMetricId(10);
public void selectByComponentUuidsAndMetricIds_returns_empty_list_if_metric_does_not_match() {
LiveMeasureDto measure = newLiveMeasure().setMetricId(metric.getId());
underTest.insert(db.getSession(), measure);

List<LiveMeasureDto> selected = underTest.selectByComponentUuids(db.getSession(), singletonList(measure.getComponentUuid()), singletonList(222));
int otherMetricId = metric.getId() + 100;
List<LiveMeasureDto> selected = underTest.selectByComponentUuidsAndMetricIds(db.getSession(), singletonList(measure.getComponentUuid()), singletonList(otherMetricId));

assertThat(selected).isEmpty();
}

@Test
public void selectByComponentUuids_returns_empty_list_if_component_does_not_match() {
public void selectByComponentUuidsAndMetricIds_returns_empty_list_if_component_does_not_match() {
LiveMeasureDto measure = newLiveMeasure();
underTest.insert(db.getSession(), measure);

List<LiveMeasureDto> selected = underTest.selectByComponentUuids(db.getSession(), singletonList("_missing_"), singletonList(measure.getMetricId()));
List<LiveMeasureDto> selected = underTest.selectByComponentUuidsAndMetricIds(db.getSession(), singletonList("_missing_"), singletonList(measure.getMetricId()));

assertThat(selected).isEmpty();
}

@Test
public void test_selectByComponentUuidsAndMetricKeys() {
LiveMeasureDto measure1 = newLiveMeasure().setMetricId(metric.getId());
LiveMeasureDto measure2 = newLiveMeasure().setMetricId(metric.getId());
underTest.insert(db.getSession(), measure1);
underTest.insert(db.getSession(), measure2);

List<LiveMeasureDto> selected = underTest.selectByComponentUuidsAndMetricKeys(db.getSession(), asList(measure1.getComponentUuid(), measure2.getComponentUuid()),
singletonList(metric.getKey()));
assertThat(selected)
.extracting(LiveMeasureDto::getComponentUuid, LiveMeasureDto::getProjectUuid, LiveMeasureDto::getMetricId, LiveMeasureDto::getValue, LiveMeasureDto::getDataAsString)
.containsExactlyInAnyOrder(
tuple(measure1.getComponentUuid(), measure1.getProjectUuid(), measure1.getMetricId(), measure1.getValue(), measure1.getDataAsString()),
tuple(measure2.getComponentUuid(), measure2.getProjectUuid(), measure2.getMetricId(), measure2.getValue(), measure2.getDataAsString()));

assertThat(underTest.selectByComponentUuidsAndMetricKeys(db.getSession(), emptyList(), singletonList(metric.getKey()))).isEmpty();
assertThat(underTest.selectByComponentUuidsAndMetricKeys(db.getSession(), singletonList(measure1.getComponentUuid()), emptyList())).isEmpty();
}

@Test
public void selectByComponentUuidsAndMetricKeys_returns_empty_list_if_metric_does_not_match() {
LiveMeasureDto measure = newLiveMeasure().setMetricId(metric.getId());
underTest.insert(db.getSession(), measure);

List<LiveMeasureDto> selected = underTest.selectByComponentUuidsAndMetricKeys(db.getSession(), singletonList(measure.getComponentUuid()), singletonList("_other_"));

assertThat(selected).isEmpty();
}

@Test
public void selectByComponentUuidsAndMetricKeys_returns_empty_list_if_component_does_not_match() {
LiveMeasureDto measure = newLiveMeasure().setMetricId(metric.getId());
underTest.insert(db.getSession(), measure);

List<LiveMeasureDto> selected = underTest.selectByComponentUuidsAndMetricKeys(db.getSession(), singletonList("_missing_"), singletonList(metric.getKey()));

assertThat(selected).isEmpty();
}
@@ -96,6 +152,68 @@ public class LiveMeasureDaoTest {
.isEqualToComparingFieldByField(stored);
}

@Test
public void selectTreeByQuery() {
List<LiveMeasureDto> results = new ArrayList<>();
MetricDto metric = db.measures().insertMetric();
ComponentDto project = db.components().insertPrivateProject();
ComponentDto file = db.components().insertComponent(newFileDto(project));
underTest.insert(db.getSession(), newLiveMeasure(file, metric).setValue(3.14));

underTest.selectTreeByQuery(db.getSession(), project,
MeasureTreeQuery.builder()
.setMetricIds(singleton(metric.getId()))
.setStrategy(MeasureTreeQuery.Strategy.LEAVES).build(),
context -> results.add(context.getResultObject()));

assertThat(results).hasSize(1);
LiveMeasureDto result = results.get(0);
assertThat(result.getComponentUuid()).isEqualTo(file.uuid());
assertThat(result.getMetricId()).isEqualTo(metric.getId());
assertThat(result.getValue()).isEqualTo(3.14);
}

@Test
public void selectTreeByQuery_with_empty_results() {
List<LiveMeasureDto> results = new ArrayList<>();
underTest.selectTreeByQuery(db.getSession(), newPrivateProjectDto(db.getDefaultOrganization()),
MeasureTreeQuery.builder().setStrategy(MeasureTreeQuery.Strategy.LEAVES).build(),
context -> results.add(context.getResultObject()));

assertThat(results).isEmpty();
}

@Test
public void selectMeasure_map_fields() {
MetricDto metric = db.measures().insertMetric();
ComponentDto project = db.components().insertPrivateProject();
ComponentDto file = db.components().insertComponent(newFileDto(project));
underTest.insert(db.getSession(), newLiveMeasure(file, metric).setValue(3.14).setVariation(0.1).setData("text_value"));

LiveMeasureDto result = underTest.selectMeasure(db.getSession(), file.uuid(), metric.getKey()).orElseThrow(() -> new IllegalArgumentException("Measure not found"));

assertThat(result).as("Fail to map fields of %s", result.toString()).extracting(
LiveMeasureDto::getProjectUuid, LiveMeasureDto::getComponentUuid, LiveMeasureDto::getMetricId, LiveMeasureDto::getValue, LiveMeasureDto::getVariation,
LiveMeasureDto::getDataAsString, LiveMeasureDto::getTextValue)
.contains(project.uuid(), file.uuid(), metric.getId(), 3.14, 0.1, "text_value", "text_value");
}

@Test
public void insert_data() {
byte[] data = "text_value".getBytes(StandardCharsets.UTF_8);
MetricDto metric = db.measures().insertMetric();
ComponentDto project = db.components().insertPrivateProject();
ComponentDto file = db.components().insertComponent(newFileDto(project));
LiveMeasureDto measure = newLiveMeasure(file, metric).setData(data);

underTest.insert(db.getSession(), measure);


LiveMeasureDto result = underTest.selectMeasure(db.getSession(), file.uuid(), metric.getKey()).orElseThrow(() -> new IllegalArgumentException("Measure not found"));
assertThat(new String(result.getData(), StandardCharsets.UTF_8)).isEqualTo("text_value");
assertThat(result.getDataAsString()).isEqualTo("text_value");
}

@Test
public void test_insertOrUpdate() {
// insert
@@ -139,7 +257,7 @@ public class LiveMeasureDaoTest {
}

private void verifyPersisted(LiveMeasureDto dto) {
List<LiveMeasureDto> selected = underTest.selectByComponentUuids(db.getSession(), singletonList(dto.getComponentUuid()), singletonList(dto.getMetricId()));
List<LiveMeasureDto> selected = underTest.selectByComponentUuidsAndMetricIds(db.getSession(), singletonList(dto.getComponentUuid()), singletonList(dto.getMetricId()));
assertThat(selected).hasSize(1);
assertThat(selected.get(0)).isEqualToComparingFieldByField(dto);
}

+ 2
- 2
server/sonar-db-dao/src/test/java/org/sonar/db/purge/PurgeDaoTest.java Visa fil

@@ -412,8 +412,8 @@ public class PurgeDaoTest {

underTest.deleteProject(dbSession, project1.uuid());

assertThat(dbClient.liveMeasureDao().selectByComponentUuids(dbSession, asList(project1.uuid(), module1.uuid()), asList(metric.getId()))).isEmpty();
assertThat(dbClient.liveMeasureDao().selectByComponentUuids(dbSession, asList(project2.uuid(), module2.uuid()), asList(metric.getId()))).hasSize(2);
assertThat(dbClient.liveMeasureDao().selectByComponentUuidsAndMetricIds(dbSession, asList(project1.uuid(), module1.uuid()), asList(metric.getId()))).isEmpty();
assertThat(dbClient.liveMeasureDao().selectByComponentUuidsAndMetricIds(dbSession, asList(project2.uuid(), module2.uuid()), asList(metric.getId()))).hasSize(2);
}

private void verifyNoEffect(ComponentDto firstRoot, ComponentDto... otherRoots) {

+ 2
- 1
server/sonar-server/src/main/java/org/sonar/server/component/index/ComponentIndexer.java Visa fil

@@ -84,9 +84,10 @@ public class ComponentIndexer implements ProjectIndexer, NeedAuthorizationIndexe
@Override
public Collection<EsQueueDto> prepareForRecovery(DbSession dbSession, Collection<String> projectUuids, Cause cause) {
switch (cause) {
case MEASURE_CHANGE:
case PROJECT_TAGS_UPDATE:
case PERMISSION_CHANGE:
// tags and permissions are not part of type components/component
// measures, tags and permissions are not part of type components/component
return emptyList();

case PROJECT_CREATION:

+ 1
- 1
server/sonar-server/src/main/java/org/sonar/server/component/ws/AppAction.java Visa fil

@@ -190,7 +190,7 @@ public class AppAction implements ComponentsWsAction {
List<MetricDto> metrics = dbClient.metricDao().selectByKeys(dbSession, METRIC_KEYS);
Map<Integer, MetricDto> metricsById = Maps.uniqueIndex(metrics, MetricDto::getId);
List<LiveMeasureDto> measures = dbClient.liveMeasureDao()
.selectByComponentUuids(dbSession, Collections.singletonList(component.uuid()), metricsById.keySet());
.selectByComponentUuidsAndMetricIds(dbSession, Collections.singletonList(component.uuid()), metricsById.keySet());
return Maps.uniqueIndex(measures, m -> metricsById.get(m.getMetricId()).getKey());
}


+ 1
- 1
server/sonar-server/src/main/java/org/sonar/server/computation/task/projectanalysis/api/posttask/PostProjectAnalysisTasksExecutor.java Visa fil

@@ -163,7 +163,7 @@ public class PostProjectAnalysisTasksExecutor implements ComputationStepExecutor

@CheckForNull
private QualityGateImpl createQualityGate() {
com.google.common.base.Optional<org.sonar.server.computation.task.projectanalysis.qualitygate.QualityGate> qualityGateOptional = this.qualityGateHolder.getQualityGate();
Optional<org.sonar.server.computation.task.projectanalysis.qualitygate.QualityGate> qualityGateOptional = this.qualityGateHolder.getQualityGate();
if (qualityGateOptional.isPresent()) {
org.sonar.server.computation.task.projectanalysis.qualitygate.QualityGate qualityGate = qualityGateOptional.get();


+ 1
- 1
server/sonar-server/src/main/java/org/sonar/server/computation/task/projectanalysis/formula/counter/RatingValue.java Visa fil

@@ -20,7 +20,7 @@
package org.sonar.server.computation.task.projectanalysis.formula.counter;

import javax.annotation.Nullable;
import org.sonar.server.computation.task.projectanalysis.qualitymodel.RatingGrid.Rating;
import org.sonar.server.computation.task.projectanalysis.qualitymodel.Rating;

/**
* Convenience class wrapping a rating to compute the value and know it is has ever been set.

+ 6
- 6
server/sonar-server/src/main/java/org/sonar/server/computation/task/projectanalysis/qualitygate/MutableQualityGateHolder.java Visa fil

@@ -19,14 +19,12 @@
*/
package org.sonar.server.computation.task.projectanalysis.qualitygate;

import org.sonar.server.computation.task.projectanalysis.component.Component;
import org.sonar.server.qualitygate.EvaluatedQualityGate;

public interface MutableQualityGateHolder extends QualityGateHolder {
/**
* Sets the quality gate.
* Settings a quality gate more than once is not allowed and it can never be set to {@code null}.
*
* @param qualityGate a {@link Component}, can not be {@code null}
* Setting a quality gate more than once is not allowed and it can never be set to {@code null}.
*
* @throws NullPointerException if {@code qualityGate} is {@code null}
* @throws IllegalStateException if the holder has already been initialized
@@ -34,9 +32,11 @@ public interface MutableQualityGateHolder extends QualityGateHolder {
void setQualityGate(QualityGate qualityGate);

/**
* Sets that there is no quality gate for the project of the currently processed {@link ReportQueue.Item}.
* Sets the evaluation of quality gate.
* Setting more than once is not allowed and it can never be set to {@code null}.
*
* @throws NullPointerException if {@code qualityGate} is {@code null}
* @throws IllegalStateException if the holder has already been initialized
*/
void setNoQualityGate();
void setEvaluation(EvaluatedQualityGate evaluation);
}

+ 9
- 1
server/sonar-server/src/main/java/org/sonar/server/computation/task/projectanalysis/qualitygate/QualityGateHolder.java Visa fil

@@ -19,7 +19,8 @@
*/
package org.sonar.server.computation.task.projectanalysis.qualitygate;

import com.google.common.base.Optional;
import java.util.Optional;
import org.sonar.server.qualitygate.EvaluatedQualityGate;

public interface QualityGateHolder {
/**
@@ -28,4 +29,11 @@ public interface QualityGateHolder {
* @throws IllegalStateException if the holder has not been initialized (ie. we don't know yet what is the QualityGate)
*/
Optional<QualityGate> getQualityGate();

/**
* Evaluation of quality gate, including status and condition details.
*
* @throws IllegalStateException if the holder has not been initialized (ie. gate has not been evaluated yet)
*/
Optional<EvaluatedQualityGate> getEvaluation();
}

+ 21
- 22
server/sonar-server/src/main/java/org/sonar/server/computation/task/projectanalysis/qualitygate/QualityGateHolderImpl.java Visa fil

@@ -19,44 +19,43 @@
*/
package org.sonar.server.computation.task.projectanalysis.qualitygate;

import com.google.common.base.Optional;
import javax.annotation.CheckForNull;
import java.util.Optional;
import org.sonar.server.qualitygate.EvaluatedQualityGate;

import static com.google.common.base.Optional.absent;
import static com.google.common.base.Optional.of;
import static com.google.common.base.Preconditions.checkState;
import static java.util.Objects.requireNonNull;

public class QualityGateHolderImpl implements MutableQualityGateHolder {
private boolean initialized = false;
@CheckForNull
private Optional<QualityGate> qualityGate;
private QualityGate qualityGate;
private EvaluatedQualityGate evaluation;

@Override
public void setQualityGate(QualityGate qualityGate) {
public void setQualityGate(QualityGate g) {
// fail fast
requireNonNull(qualityGate);
checkNotInitialized();
requireNonNull(g);
checkState(qualityGate == null, "QualityGateHolder can be initialized only once");

this.initialized = true;
this.qualityGate = of(qualityGate);
this.qualityGate = g;
}

@Override
public void setNoQualityGate() {
checkNotInitialized();

this.initialized = true;
this.qualityGate = absent();
public Optional<QualityGate> getQualityGate() {
checkState(qualityGate != null, "QualityGate has not been set yet");
return Optional.of(qualityGate);
}

private void checkNotInitialized() {
checkState(!initialized, "QualityGateHolder can be initialized only once");
@Override
public void setEvaluation(EvaluatedQualityGate g) {
// fail fast
requireNonNull(g);
checkState(evaluation == null, "QualityGateHolder evaluation can be initialized only once");

this.evaluation = g;
}

@Override
public Optional<QualityGate> getQualityGate() {
checkState(initialized, "QualityGate has not been set yet");
return qualityGate;
public Optional<EvaluatedQualityGate> getEvaluation() {
checkState(evaluation != null, "Evaluation of QualityGate has not been set yet");
return Optional.of(evaluation);
}
}

+ 3
- 2
server/sonar-server/src/main/java/org/sonar/server/computation/task/projectanalysis/qualitygate/QualityGateService.java Visa fil

@@ -30,8 +30,9 @@ public interface QualityGateService {
Optional<QualityGate> findById(long id);

/**
* Retrieve the {@link QualityGate} from the database with the specified uuid, if it exists.
* Retrieve the {@link QualityGate} from the database with the specified uuid.
* @throws IllegalStateException if database is corrupted and default gate can't be found.
*/
Optional<QualityGate> findDefaultQualityGate(Organization organizationDto);
QualityGate findDefaultQualityGate(Organization organizationDto);

}

+ 3
- 3
server/sonar-server/src/main/java/org/sonar/server/computation/task/projectanalysis/qualitygate/QualityGateServiceImpl.java Visa fil

@@ -57,13 +57,13 @@ public class QualityGateServiceImpl implements QualityGateService {
}

@Override
public Optional<QualityGate> findDefaultQualityGate(Organization organization) {
public QualityGate findDefaultQualityGate(Organization organization) {
try (DbSession dbSession = dbClient.openSession(false)) {
QualityGateDto qualityGateDto = dbClient.qualityGateDao().selectByOrganizationAndUuid(dbSession, organization.toDto(), organization.getDefaultQualityGateUuid());
if (qualityGateDto == null) {
return Optional.empty();
throw new IllegalStateException("The default Quality gate is missing on organization " + organization.getKey());
}
return Optional.of(toQualityGate(dbSession, qualityGateDto));
return toQualityGate(dbSession, qualityGateDto);
}
}


server/sonar-server/src/main/java/org/sonar/server/computation/task/projectanalysis/qualitymodel/RatingGrid.java → server/sonar-server/src/main/java/org/sonar/server/computation/task/projectanalysis/qualitymodel/DebtRatingGrid.java Visa fil

@@ -21,20 +21,34 @@ package org.sonar.server.computation.task.projectanalysis.qualitymodel;

import com.google.common.annotations.VisibleForTesting;
import java.util.Arrays;
import org.sonar.api.config.Configuration;
import org.sonar.api.utils.MessageException;

import static java.lang.String.format;
import static java.util.Arrays.stream;
import static org.sonar.api.CoreProperties.RATING_GRID;
import static org.sonar.api.CoreProperties.RATING_GRID_DEF_VALUES;

public class RatingGrid {
public class DebtRatingGrid {

private final double[] gridValues;

RatingGrid(double[] gridValues) {
public DebtRatingGrid(double[] gridValues) {
this.gridValues = Arrays.copyOf(gridValues, gridValues.length);
}

Rating getRatingForDensity(double density) {
public DebtRatingGrid(Configuration config) {
try {
String[] grades = config.getStringArray(RATING_GRID);
gridValues = new double[4];
for (int i = 0; i < 4; i++) {
gridValues[i] = Double.parseDouble(grades[i]);
}
} catch (Exception e) {
throw new IllegalArgumentException("The rating grid is incorrect. Expected something similar to '"
+ RATING_GRID_DEF_VALUES + "' and got '" + config.get(RATING_GRID).orElse("") + "'", e);
}
}

public Rating getRatingForDensity(double density) {
for (Rating rating : Rating.values()) {
double lowerBound = getGradeLowerBound(rating);
if (density >= lowerBound) {
@@ -44,7 +58,7 @@ public class RatingGrid {
throw MessageException.of("The rating density value should be between 0 and " + Double.MAX_VALUE + " and got " + density);
}

double getGradeLowerBound(Rating rating) {
public double getGradeLowerBound(Rating rating) {
if (rating.getIndex() > 1) {
return gridValues[rating.getIndex() - 2];
}
@@ -56,30 +70,4 @@ public class RatingGrid {
return gridValues;
}

public enum Rating {
E(5),
D(4),
C(3),
B(2),
A(1);

private final int index;

Rating(int index) {
this.index = index;
}

public int getIndex() {
return index;
}

public static Rating valueOf(int index) {
return stream(Rating.values()).filter(r -> r.getIndex() == index).findFirst().orElseThrow(() -> new IllegalArgumentException(format("Unknown value '%s'", index)));
}

public static boolean isValidRating(String value) {
return stream(Rating.values()).anyMatch(r -> r.name().equals(value));
}
}

}

+ 2
- 5
server/sonar-server/src/main/java/org/sonar/server/computation/task/projectanalysis/qualitymodel/MaintainabilityMeasuresVisitor.java Visa fil

@@ -29,7 +29,6 @@ import org.sonar.server.computation.task.projectanalysis.measure.Measure;
import org.sonar.server.computation.task.projectanalysis.measure.MeasureRepository;
import org.sonar.server.computation.task.projectanalysis.metric.Metric;
import org.sonar.server.computation.task.projectanalysis.metric.MetricRepository;
import org.sonar.server.computation.task.projectanalysis.qualitymodel.RatingGrid.Rating;

import static org.sonar.api.measures.CoreMetrics.DEVELOPMENT_COST_KEY;
import static org.sonar.api.measures.CoreMetrics.EFFORT_TO_REACH_MAINTAINABILITY_RATING_A_KEY;
@@ -50,7 +49,6 @@ import static org.sonar.server.computation.task.projectanalysis.measure.Measure.
public class MaintainabilityMeasuresVisitor extends PathAwareVisitorAdapter<MaintainabilityMeasuresVisitor.Counter> {
private final MeasureRepository measureRepository;
private final RatingSettings ratingSettings;
private final RatingGrid ratingGrid;

private final Metric nclocMetric;
private final Metric developmentCostMetric;
@@ -64,7 +62,6 @@ public class MaintainabilityMeasuresVisitor extends PathAwareVisitorAdapter<Main
super(CrawlerDepthLimit.FILE, POST_ORDER, CounterFactory.INSTANCE);
this.measureRepository = measureRepository;
this.ratingSettings = ratingSettings;
this.ratingGrid = ratingSettings.getRatingGrid();

// Input metrics
this.nclocMetric = metricRepository.getByKey(NCLOC_KEY);
@@ -134,7 +131,7 @@ public class MaintainabilityMeasuresVisitor extends PathAwareVisitorAdapter<Main
}

private void addMaintainabilityRatingMeasure(Component component, double density) {
Rating rating = ratingGrid.getRatingForDensity(density);
Rating rating = ratingSettings.getDebtRatingGrid().getRatingForDensity(density);
measureRepository.add(component, maintainabilityRatingMetric, newMeasureBuilder().create(rating.getIndex(), rating.name()));
}

@@ -142,7 +139,7 @@ public class MaintainabilityMeasuresVisitor extends PathAwareVisitorAdapter<Main
long developmentCostValue = path.current().devCosts;
Optional<Measure> effortMeasure = measureRepository.getRawMeasure(component, maintainabilityRemediationEffortMetric);
long effort = effortMeasure.isPresent() ? effortMeasure.get().getLongValue() : 0L;
long upperGradeCost = ((Double) (ratingGrid.getGradeLowerBound(Rating.B) * developmentCostValue)).longValue();
long upperGradeCost = ((Double) (ratingSettings.getDebtRatingGrid().getGradeLowerBound(Rating.B) * developmentCostValue)).longValue();
long effortToRatingA = upperGradeCost < effort ? (effort - upperGradeCost) : 0L;
measureRepository.add(component, effortToMaintainabilityRatingAMetric, Measure.newMeasureBuilder().create(effortToRatingA));
}

+ 7
- 4
server/sonar-server/src/main/java/org/sonar/server/computation/task/projectanalysis/qualitymodel/NewMaintainabilityMeasuresVisitor.java Visa fil

@@ -42,6 +42,7 @@ import org.sonar.server.computation.task.projectanalysis.scm.ScmInfo;
import org.sonar.server.computation.task.projectanalysis.scm.ScmInfoRepository;

import static org.sonar.api.measures.CoreMetrics.NCLOC_DATA_KEY;
import static org.sonar.api.measures.CoreMetrics.NEW_DEVELOPMENT_COST_KEY;
import static org.sonar.api.measures.CoreMetrics.NEW_MAINTAINABILITY_RATING_KEY;
import static org.sonar.api.measures.CoreMetrics.NEW_SQALE_DEBT_RATIO_KEY;
import static org.sonar.api.measures.CoreMetrics.NEW_TECHNICAL_DEBT_KEY;
@@ -64,22 +65,21 @@ public class NewMaintainabilityMeasuresVisitor extends PathAwareVisitorAdapter<N
private final MeasureRepository measureRepository;
private final PeriodHolder periodHolder;
private final RatingSettings ratingSettings;
private final RatingGrid ratingGrid;

private final Metric newDebtMetric;
private final Metric nclocDataMetric;

private final Metric newDevelopmentCostMetric;
private final Metric newDebtRatioMetric;
private final Metric newMaintainabilityRatingMetric;

public NewMaintainabilityMeasuresVisitor(MetricRepository metricRepository, MeasureRepository measureRepository, ScmInfoRepository scmInfoRepository,
PeriodHolder periodHolder, RatingSettings ratingSettings) {
PeriodHolder periodHolder, RatingSettings ratingSettings) {
super(CrawlerDepthLimit.FILE, POST_ORDER, CounterFactory.INSTANCE);
this.measureRepository = measureRepository;
this.scmInfoRepository = scmInfoRepository;
this.periodHolder = periodHolder;
this.ratingSettings = ratingSettings;
this.ratingGrid = ratingSettings.getRatingGrid();

// computed by NewDebtAggregator which is executed by IntegrateIssuesVisitor
this.newDebtMetric = metricRepository.getByKey(NEW_TECHNICAL_DEBT_KEY);
@@ -87,6 +87,7 @@ public class NewMaintainabilityMeasuresVisitor extends PathAwareVisitorAdapter<N
this.nclocDataMetric = metricRepository.getByKey(NCLOC_DATA_KEY);

// output metrics
this.newDevelopmentCostMetric = metricRepository.getByKey(NEW_DEVELOPMENT_COST_KEY);
this.newDebtRatioMetric = metricRepository.getByKey(NEW_SQALE_DEBT_RATIO_KEY);
this.newMaintainabilityRatingMetric = metricRepository.getByKey(NEW_MAINTAINABILITY_RATING_KEY);
}
@@ -121,7 +122,9 @@ public class NewMaintainabilityMeasuresVisitor extends PathAwareVisitorAdapter<N
}
double density = computeDensity(path.current());
double newDebtRatio = 100.0 * density;
double newMaintainability = ratingGrid.getRatingForDensity(density).getIndex();
double newMaintainability = ratingSettings.getDebtRatingGrid().getRatingForDensity(density).getIndex();
long newDevelopmentCost = path.current().getDevCost().getValue();
measureRepository.add(component, this.newDevelopmentCostMetric, newMeasureBuilder().setVariation(newDevelopmentCost).createNoValue());
measureRepository.add(component, this.newDebtRatioMetric, newMeasureBuilder().setVariation(newDebtRatio).createNoValue());
measureRepository.add(component, this.newMaintainabilityRatingMetric, newMeasureBuilder().setVariation(newMaintainability).createNoValue());
}

+ 6
- 6
server/sonar-server/src/main/java/org/sonar/server/computation/task/projectanalysis/qualitymodel/NewReliabilityAndSecurityRatingMeasuresVisitor.java Visa fil

@@ -47,12 +47,12 @@ import static org.sonar.api.utils.DateUtils.truncateToSeconds;
import static org.sonar.server.computation.task.projectanalysis.component.ComponentVisitor.Order.POST_ORDER;
import static org.sonar.server.computation.task.projectanalysis.component.CrawlerDepthLimit.LEAVES;
import static org.sonar.server.computation.task.projectanalysis.measure.Measure.newMeasureBuilder;
import static org.sonar.server.computation.task.projectanalysis.qualitymodel.RatingGrid.Rating;
import static org.sonar.server.computation.task.projectanalysis.qualitymodel.RatingGrid.Rating.A;
import static org.sonar.server.computation.task.projectanalysis.qualitymodel.RatingGrid.Rating.B;
import static org.sonar.server.computation.task.projectanalysis.qualitymodel.RatingGrid.Rating.C;
import static org.sonar.server.computation.task.projectanalysis.qualitymodel.RatingGrid.Rating.D;
import static org.sonar.server.computation.task.projectanalysis.qualitymodel.RatingGrid.Rating.E;
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;

/**
* Compute following measures :

+ 63
- 0
server/sonar-server/src/main/java/org/sonar/server/computation/task/projectanalysis/qualitymodel/Rating.java Visa fil

@@ -0,0 +1,63 @@
/*
* 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.computation.task.projectanalysis.qualitymodel;

import com.google.common.collect.ImmutableMap;
import java.util.Map;

import static java.lang.String.format;
import static java.util.Arrays.stream;
import static org.sonar.api.rule.Severity.BLOCKER;
import static org.sonar.api.rule.Severity.CRITICAL;
import static org.sonar.api.rule.Severity.INFO;
import static org.sonar.api.rule.Severity.MAJOR;
import static org.sonar.api.rule.Severity.MINOR;

public enum Rating {
E(5),
D(4),
C(3),
B(2),
A(1);

private final int index;

Rating(int index) {
this.index = index;
}

public int getIndex() {
return index;
}

public static Rating valueOf(int index) {
return stream(Rating.values())
.filter(r -> r.getIndex() == index)
.findFirst()
.orElseThrow(() -> new IllegalArgumentException(format("Unknown value '%s'", index)));
}

public static final Map<String, Rating> RATING_BY_SEVERITY = ImmutableMap.of(
BLOCKER, E,
CRITICAL, D,
MAJOR, C,
MINOR, B,
INFO, A);
}

+ 29
- 38
server/sonar-server/src/main/java/org/sonar/server/computation/task/projectanalysis/qualitymodel/RatingSettings.java Visa fil

@@ -24,6 +24,7 @@ import java.util.Map;
import javax.annotation.CheckForNull;
import javax.annotation.Nullable;
import javax.annotation.concurrent.Immutable;
import org.sonar.api.ce.ComputeEngineSide;
import org.sonar.api.config.Configuration;
import org.sonar.api.utils.MessageException;

@@ -33,45 +34,22 @@ import static org.sonar.api.CoreProperties.LANGUAGE_SPECIFIC_PARAMETERS;
import static org.sonar.api.CoreProperties.LANGUAGE_SPECIFIC_PARAMETERS_LANGUAGE_KEY;
import static org.sonar.api.CoreProperties.LANGUAGE_SPECIFIC_PARAMETERS_MAN_DAYS_KEY;
import static org.sonar.api.CoreProperties.LANGUAGE_SPECIFIC_PARAMETERS_SIZE_METRIC_KEY;
import static org.sonar.api.CoreProperties.RATING_GRID;
import static org.sonar.api.CoreProperties.RATING_GRID_DEF_VALUES;

@ComputeEngineSide
public class RatingSettings {

private final Configuration config;
private final DebtRatingGrid ratingGrid;
private final long defaultDevCost;
private final Map<String, LanguageSpecificConfiguration> languageSpecificConfigurationByLanguageKey;

public RatingSettings(Configuration config) {
this.config = config;
this.languageSpecificConfigurationByLanguageKey = buildLanguageSpecificConfigurationByLanguageKey(config);
ratingGrid = new DebtRatingGrid(config);
defaultDevCost = initDefaultDevelopmentCost(config);
languageSpecificConfigurationByLanguageKey = initLanguageSpecificConfigurationByLanguageKey(config);
}

private static Map<String, LanguageSpecificConfiguration> buildLanguageSpecificConfigurationByLanguageKey(Configuration config) {
ImmutableMap.Builder<String, LanguageSpecificConfiguration> builder = ImmutableMap.builder();
String[] languageConfigIndexes = config.getStringArray(LANGUAGE_SPECIFIC_PARAMETERS);
for (String languageConfigIndex : languageConfigIndexes) {
String languagePropertyKey = LANGUAGE_SPECIFIC_PARAMETERS + "." + languageConfigIndex + "." + LANGUAGE_SPECIFIC_PARAMETERS_LANGUAGE_KEY;
String languageKey = config.get(languagePropertyKey)
.orElseThrow(() -> MessageException.of("Technical debt configuration is corrupted. At least one language specific parameter has no Language key. " +
"Contact your administrator to update this configuration in the global administration section of SonarQube."));
builder.put(languageKey, LanguageSpecificConfiguration.create(config, languageConfigIndex));
}
return builder.build();
}

public RatingGrid getRatingGrid() {
try {
String[] ratingGrades = config.getStringArray(RATING_GRID);
double[] grid = new double[4];
for (int i = 0; i < 4; i++) {
grid[i] = Double.parseDouble(ratingGrades[i]);
}
return new RatingGrid(grid);
} catch (Exception e) {
throw new IllegalArgumentException("The rating grid is incorrect. Expected something similar to '"
+ RATING_GRID_DEF_VALUES + "' and got '"
+ config.get(RATING_GRID).get() + "'", e);
}
public DebtRatingGrid getDebtRatingGrid() {
return ratingGrid;
}

public long getDevCost(@Nullable String languageKey) {
@@ -86,10 +64,28 @@ public class RatingSettings {
}
}

return getDefaultDevelopmentCost();
return defaultDevCost;
}

private long getDefaultDevelopmentCost() {
@CheckForNull
private LanguageSpecificConfiguration getSpecificParametersForLanguage(String languageKey) {
return languageSpecificConfigurationByLanguageKey.get(languageKey);
}

private static Map<String, LanguageSpecificConfiguration> initLanguageSpecificConfigurationByLanguageKey(Configuration config) {
ImmutableMap.Builder<String, LanguageSpecificConfiguration> builder = ImmutableMap.builder();
String[] languageConfigIndexes = config.getStringArray(LANGUAGE_SPECIFIC_PARAMETERS);
for (String languageConfigIndex : languageConfigIndexes) {
String languagePropertyKey = LANGUAGE_SPECIFIC_PARAMETERS + "." + languageConfigIndex + "." + LANGUAGE_SPECIFIC_PARAMETERS_LANGUAGE_KEY;
String languageKey = config.get(languagePropertyKey)
.orElseThrow(() -> MessageException.of("Technical debt configuration is corrupted. At least one language specific parameter has no Language key. " +
"Contact your administrator to update this configuration in the global administration section of SonarQube."));
builder.put(languageKey, LanguageSpecificConfiguration.create(config, languageConfigIndex));
}
return builder.build();
}

private static long initDefaultDevelopmentCost(Configuration config) {
try {
return Long.parseLong(config.get(DEVELOPMENT_COST).get());
} catch (NumberFormatException e) {
@@ -98,11 +94,6 @@ public class RatingSettings {
}
}

@CheckForNull
private LanguageSpecificConfiguration getSpecificParametersForLanguage(String languageKey) {
return languageSpecificConfigurationByLanguageKey.get(languageKey);
}

@Immutable
private static class LanguageSpecificConfiguration {
private final String language;

+ 23
- 55
server/sonar-server/src/main/java/org/sonar/server/computation/task/projectanalysis/qualitymodel/ReliabilityAndSecurityRatingMeasuresVisitor.java Visa fil

@@ -21,35 +21,23 @@ package org.sonar.server.computation.task.projectanalysis.qualitymodel;

import com.google.common.collect.ImmutableMap;
import java.util.Map;
import org.sonar.api.ce.measure.Issue;
import org.sonar.api.measures.CoreMetrics;
import org.sonar.server.computation.task.projectanalysis.component.Component;
import org.sonar.server.computation.task.projectanalysis.component.PathAwareVisitorAdapter;
import org.sonar.server.computation.task.projectanalysis.formula.counter.RatingValue;
import org.sonar.server.computation.task.projectanalysis.issue.ComponentIssuesRepository;
import org.sonar.server.computation.task.projectanalysis.measure.Measure;
import org.sonar.server.computation.task.projectanalysis.measure.MeasureRepository;
import org.sonar.server.computation.task.projectanalysis.metric.Metric;
import org.sonar.server.computation.task.projectanalysis.metric.MetricRepository;

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.rule.Severity.BLOCKER;
import static org.sonar.api.rule.Severity.CRITICAL;
import static org.sonar.api.rule.Severity.INFO;
import static org.sonar.api.rule.Severity.MAJOR;
import static org.sonar.api.rule.Severity.MINOR;
import static org.sonar.api.rules.RuleType.BUG;
import static org.sonar.api.rules.RuleType.VULNERABILITY;
import static org.sonar.server.computation.task.projectanalysis.component.ComponentVisitor.Order.POST_ORDER;
import static org.sonar.server.computation.task.projectanalysis.component.CrawlerDepthLimit.FILE;
import static org.sonar.server.computation.task.projectanalysis.measure.Measure.newMeasureBuilder;
import static org.sonar.server.computation.task.projectanalysis.qualitymodel.RatingGrid.Rating;
import static org.sonar.server.computation.task.projectanalysis.qualitymodel.RatingGrid.Rating.A;
import static org.sonar.server.computation.task.projectanalysis.qualitymodel.RatingGrid.Rating.B;
import static org.sonar.server.computation.task.projectanalysis.qualitymodel.RatingGrid.Rating.C;
import static org.sonar.server.computation.task.projectanalysis.qualitymodel.RatingGrid.Rating.D;
import static org.sonar.server.computation.task.projectanalysis.qualitymodel.RatingGrid.Rating.E;
import static org.sonar.server.computation.task.projectanalysis.qualitymodel.Rating.RATING_BY_SEVERITY;

/**
* Compute following measures for projects and descendants:
@@ -58,20 +46,8 @@ import static org.sonar.server.computation.task.projectanalysis.qualitymodel.Rat
*/
public class ReliabilityAndSecurityRatingMeasuresVisitor extends PathAwareVisitorAdapter<ReliabilityAndSecurityRatingMeasuresVisitor.Counter> {

private static final Map<String, Rating> RATING_BY_SEVERITY = ImmutableMap.of(
BLOCKER, E,
CRITICAL, D,
MAJOR, C,
MINOR, B,
INFO, A);

private final MeasureRepository measureRepository;
private final ComponentIssuesRepository componentIssuesRepository;

// Output metrics
private final Metric reliabilityRatingMetric;
private final Metric securityRatingMetric;

private final Map<String, Metric> metricsByKey;

public ReliabilityAndSecurityRatingMeasuresVisitor(MetricRepository metricRepository, MeasureRepository measureRepository, ComponentIssuesRepository componentIssuesRepository) {
@@ -80,8 +56,8 @@ public class ReliabilityAndSecurityRatingMeasuresVisitor extends PathAwareVisito
this.componentIssuesRepository = componentIssuesRepository;

// Output metrics
this.reliabilityRatingMetric = metricRepository.getByKey(RELIABILITY_RATING_KEY);
this.securityRatingMetric = metricRepository.getByKey(SECURITY_RATING_KEY);
Metric reliabilityRatingMetric = metricRepository.getByKey(RELIABILITY_RATING_KEY);
Metric securityRatingMetric = metricRepository.getByKey(SECURITY_RATING_KEY);

this.metricsByKey = ImmutableMap.of(
RELIABILITY_RATING_KEY, reliabilityRatingMetric,
@@ -94,13 +70,13 @@ public class ReliabilityAndSecurityRatingMeasuresVisitor extends PathAwareVisito
}

@Override
public void visitDirectory(Component directory, Path<Counter> path) {
computeAndSaveMeasures(directory, path);
public void visitModule(Component module, Path<Counter> path) {
computeAndSaveMeasures(module, path);
}

@Override
public void visitModule(Component module, Path<Counter> path) {
computeAndSaveMeasures(module, path);
public void visitDirectory(Component directory, Path<Counter> path) {
computeAndSaveMeasures(directory, path);
}

@Override
@@ -110,27 +86,27 @@ public class ReliabilityAndSecurityRatingMeasuresVisitor extends PathAwareVisito

private void computeAndSaveMeasures(Component component, Path<Counter> path) {
processIssues(component, path);
path.current().ratingValueByMetric.entrySet().forEach(
entry -> measureRepository.add(component, metricsByKey.get(entry.getKey()), createRatingMeasure(entry.getValue().getValue())));
addToParent(path);
path.current().ratingValueByMetric.forEach((key, value) -> {
Rating rating = value.getValue();
measureRepository.add(component, metricsByKey.get(key), newMeasureBuilder().create(rating.getIndex(), rating.name()));
});
if (!path.isRoot()) {
path.parent().add(path.current());
}
}

private void processIssues(Component component, Path<Counter> path) {
componentIssuesRepository.getIssues(component)
.stream()
.filter(issue -> issue.resolution() == null)
.filter(issue -> issue.type().equals(BUG) || issue.type().equals(VULNERABILITY))
.forEach(path.current()::processIssue);
}

private static void addToParent(Path<Counter> path) {
if (!path.isRoot()) {
path.parent().add(path.current());
}
}

private static Measure createRatingMeasure(Rating rating) {
return newMeasureBuilder().create(rating.getIndex(), rating.name());
.forEach(issue -> {
Rating rating = RATING_BY_SEVERITY.get(issue.severity());
if (issue.type().equals(BUG)) {
path.current().ratingValueByMetric.get(RELIABILITY_RATING_KEY).increment(rating);
} else if (issue.type().equals(VULNERABILITY)) {
path.current().ratingValueByMetric.get(SECURITY_RATING_KEY).increment(rating);
}
});
}

static final class Counter {
@@ -143,17 +119,9 @@ public class ReliabilityAndSecurityRatingMeasuresVisitor extends PathAwareVisito
}

void add(Counter otherCounter) {
ratingValueByMetric.entrySet().forEach(e -> e.getValue().increment(otherCounter.ratingValueByMetric.get(e.getKey())));
ratingValueByMetric.forEach((key, value) -> value.increment(otherCounter.ratingValueByMetric.get(key)));
}

void processIssue(Issue issue) {
Rating rating = RATING_BY_SEVERITY.get(issue.severity());
if (issue.type().equals(BUG)) {
ratingValueByMetric.get(RELIABILITY_RATING_KEY).increment(rating);
} else if (issue.type().equals(VULNERABILITY)) {
ratingValueByMetric.get(SECURITY_RATING_KEY).increment(rating);
}
}
}

private static final class CounterFactory extends PathAwareVisitorAdapter.SimpleStackElementFactory<ReliabilityAndSecurityRatingMeasuresVisitor.Counter> {

+ 3
- 7
server/sonar-server/src/main/java/org/sonar/server/computation/task/projectanalysis/step/LoadQualityGateStep.java Visa fil

@@ -59,15 +59,11 @@ public class LoadQualityGateStep implements ComputationStep {
qualityGate = getProjectQualityGate();
if (!qualityGate.isPresent()) {
// No QG defined for the project, let's retrieve the QG on the organization
qualityGate = getOrganizationDefaultQualityGate();
qualityGate = Optional.of(getOrganizationDefaultQualityGate());
}
}

if (qualityGate.isPresent()) {
qualityGateHolder.setQualityGate(qualityGate.get());
} else {
qualityGateHolder.setNoQualityGate();
}
qualityGateHolder.setQualityGate(qualityGate.orElseThrow(() -> new IllegalStateException("Quality gate not present")));
}

private Optional<QualityGate> getShortLivingBranchQualityGate() {
@@ -100,7 +96,7 @@ public class LoadQualityGateStep implements ComputationStep {
}
}

private Optional<QualityGate> getOrganizationDefaultQualityGate() {
private QualityGate getOrganizationDefaultQualityGate() {
return qualityGateService.findDefaultQualityGate(analysisMetadataHolder.getOrganization());
}


+ 2
- 2
server/sonar-server/src/main/java/org/sonar/server/computation/task/projectanalysis/step/QualityGateMeasuresStep.java Visa fil

@@ -20,7 +20,6 @@
package org.sonar.server.computation.task.projectanalysis.step;

import com.google.common.base.Function;
import com.google.common.base.Optional;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.Multimap;
import com.google.common.collect.Ordering;
@@ -28,6 +27,7 @@ import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
import javax.annotation.Nonnull;
import javax.annotation.Nullable;
@@ -177,7 +177,7 @@ public class QualityGateMeasuresStep implements ComputationStep {
boolean ignoredConditions = false;
for (Map.Entry<Metric, Collection<Condition>> entry : conditionsPerMetric.asMap().entrySet()) {
Metric metric = entry.getKey();
Optional<Measure> measure = measureRepository.getRawMeasure(project, metric);
com.google.common.base.Optional<Measure> measure = measureRepository.getRawMeasure(project, metric);
if (!measure.isPresent()) {
continue;
}

+ 2
- 1
server/sonar-server/src/main/java/org/sonar/server/computation/task/projectanalysis/webhook/WebhookPostTask.java Visa fil

@@ -24,6 +24,7 @@ import java.util.Optional;
import java.util.Set;
import org.sonar.api.ce.posttask.PostProjectAnalysisTask;
import org.sonar.api.config.Configuration;
import org.sonar.api.measures.Metric;
import org.sonar.core.util.stream.MoreCollectors;
import org.sonar.server.computation.task.projectanalysis.component.ConfigurationRepository;
import org.sonar.server.qualitygate.Condition;
@@ -80,7 +81,7 @@ public class WebhookPostTask implements PostProjectAnalysisTask {
})
.collect(MoreCollectors.toSet());
return builder.setQualityGate(new org.sonar.server.qualitygate.QualityGate(qg.getId(), qg.getName(), conditions))
.setStatus(EvaluatedQualityGate.Status.valueOf(qg.getStatus().name()))
.setStatus(Metric.Level.valueOf(qg.getStatus().name()))
.build();
})
.orElse(null);

+ 2
- 1
server/sonar-server/src/main/java/org/sonar/server/es/ProjectIndexer.java Visa fil

@@ -40,7 +40,8 @@ public interface ProjectIndexer extends ResilientIndexer {
PROJECT_DELETION,
PROJECT_KEY_UPDATE,
PROJECT_TAGS_UPDATE,
PERMISSION_CHANGE
PERMISSION_CHANGE,
MEASURE_CHANGE
}

/**

+ 5
- 0
server/sonar-server/src/main/java/org/sonar/server/issue/AbstractChangeTagsAction.java Visa fil

@@ -60,6 +60,11 @@ public abstract class AbstractChangeTagsAction extends Action {

protected abstract Collection<String> getTagsToSet(Context context, Collection<String> tagsFromParams);

@Override
public boolean shouldRefreshMeasures() {
return false;
}

private Set<String> parseTags(Map<String, Object> properties) {
Set<String> result = new HashSet<>();
String tagsString = (String) properties.get(TAGS_PARAMETER);

+ 2
- 0
server/sonar-server/src/main/java/org/sonar/server/issue/Action.java Visa fil

@@ -75,6 +75,8 @@ public abstract class Action {

public abstract boolean execute(Map<String, Object> properties, Context context);

public abstract boolean shouldRefreshMeasures();

public interface Context {
DefaultIssue issue();


+ 5
- 0
server/sonar-server/src/main/java/org/sonar/server/issue/AssignAction.java Visa fil

@@ -89,6 +89,11 @@ public class AssignAction extends Action {
return isAssigneeMemberOfIssueOrganization(assignee, properties, context) && issueFieldsSetter.assign(context.issue(), assignee, context.issueChangeContext());
}

@Override
public boolean shouldRefreshMeasures() {
return false;
}

private static boolean isAssigneeMemberOfIssueOrganization(@Nullable UserDto assignee, Map<String, Object> properties, Context context) {
return assignee == null || ((Set<String>) properties.get(ASSIGNEE_ORGANIZATIONS)).contains(context.project().getOrganizationUuid());
}

+ 5
- 0
server/sonar-server/src/main/java/org/sonar/server/issue/CommentAction.java Visa fil

@@ -51,6 +51,11 @@ public class CommentAction extends Action {
return true;
}

@Override
public boolean shouldRefreshMeasures() {
return false;
}

private static String comment(Map<String, Object> properties) {
String param = (String) properties.get(COMMENT_PROPERTY);
if (Strings.isNullOrEmpty(param)) {

+ 39
- 0
server/sonar-server/src/main/java/org/sonar/server/issue/IssueChangePostProcessor.java Visa fil

@@ -0,0 +1,39 @@
/*
* 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.issue;

import java.util.Collection;
import java.util.List;
import org.sonar.api.server.ServerSide;
import org.sonar.core.issue.DefaultIssue;
import org.sonar.db.DbSession;
import org.sonar.db.component.ComponentDto;

@ServerSide
public interface IssueChangePostProcessor {

/**
* Refresh measures, quality gate status and send webhooks
*
* @param components the components of changed issues
*/
void process(DbSession dbSession, List<DefaultIssue> changedIssues, Collection<ComponentDto> components);

}

+ 46
- 0
server/sonar-server/src/main/java/org/sonar/server/issue/IssueChangePostProcessorImpl.java Visa fil

@@ -0,0 +1,46 @@
/*
* 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.issue;

import java.util.Collection;
import java.util.List;
import org.sonar.core.issue.DefaultIssue;
import org.sonar.db.DbSession;
import org.sonar.db.component.ComponentDto;
import org.sonar.server.measure.live.LiveMeasureComputer;
import org.sonar.server.qualitygate.changeevent.QGChangeEvent;
import org.sonar.server.qualitygate.changeevent.QGChangeEventListeners;

public class IssueChangePostProcessorImpl implements IssueChangePostProcessor {

private final LiveMeasureComputer liveMeasureComputer;
private final QGChangeEventListeners qualityGateListeners;

public IssueChangePostProcessorImpl(LiveMeasureComputer liveMeasureComputer, QGChangeEventListeners qualityGateListeners) {
this.liveMeasureComputer = liveMeasureComputer;
this.qualityGateListeners = qualityGateListeners;
}

@Override
public void process(DbSession dbSession, List<DefaultIssue> changedIssues, Collection<ComponentDto> components) {
List<QGChangeEvent> gateChangeEvents = liveMeasureComputer.refresh(dbSession, components);
qualityGateListeners.broadcastOnIssueChange(changedIssues, gateChangeEvents);
}
}

+ 26
- 14
server/sonar-server/src/main/java/org/sonar/server/issue/IssueUpdater.java Visa fil

@@ -19,12 +19,14 @@
*/
package org.sonar.server.issue;

import java.util.List;
import java.util.Optional;
import javax.annotation.Nullable;
import org.sonar.api.rule.RuleKey;
import org.sonar.api.rule.RuleStatus;
import org.sonar.core.issue.DefaultIssue;
import org.sonar.core.issue.IssueChangeContext;
import org.sonar.core.util.stream.MoreCollectors;
import org.sonar.db.DbClient;
import org.sonar.db.DbSession;
import org.sonar.db.component.ComponentDto;
@@ -42,38 +44,48 @@ public class IssueUpdater {
private final DbClient dbClient;
private final IssueStorage issueStorage;
private final NotificationManager notificationService;
private final IssueChangePostProcessor issueChangePostProcessor;

public IssueUpdater(DbClient dbClient, IssueStorage issueStorage, NotificationManager notificationService) {
public IssueUpdater(DbClient dbClient, IssueStorage issueStorage, NotificationManager notificationService,
IssueChangePostProcessor issueChangePostProcessor) {
this.dbClient = dbClient;
this.issueStorage = issueStorage;
this.notificationService = notificationService;
this.issueChangePostProcessor = issueChangePostProcessor;
}

/**
* Same as {@link #saveIssue(DbSession, DefaultIssue, IssueChangeContext, String)} but populates the specified
* {@link SearchResponseData} with the DTOs (rule and components) retrieved from DB to save the issue.
*/
public SearchResponseData saveIssueAndPreloadSearchResponseData(DbSession session, DefaultIssue issue, IssueChangeContext context, @Nullable String comment) {
Optional<RuleDefinitionDto> rule = getRuleByKey(session, issue.getRuleKey());
ComponentDto project = dbClient.componentDao().selectOrFailByUuid(session, issue.projectUuid());
ComponentDto component = dbClient.componentDao().selectOrFailByUuid(session, issue.componentUuid());
IssueDto issueDto = saveIssue(session, issue, context, comment, rule, project, component);
public SearchResponseData saveIssueAndPreloadSearchResponseData(DbSession dbSession, DefaultIssue issue, IssueChangeContext context,
@Nullable String comment, boolean refreshMeasures) {
Optional<RuleDefinitionDto> rule = getRuleByKey(dbSession, issue.getRuleKey());
ComponentDto project = dbClient.componentDao().selectOrFailByUuid(dbSession, issue.projectUuid());
ComponentDto component = dbClient.componentDao().selectOrFailByUuid(dbSession, issue.componentUuid());
IssueDto issueDto = doSaveIssue(dbSession, issue, context, comment, rule, project, component);

SearchResponseData result = new SearchResponseData(issueDto);
rule.ifPresent(r -> result.setRules(singletonList(r)));
result.addComponents(singleton(project));
result.addComponents(singleton(component));

if (refreshMeasures) {
List<DefaultIssue> changedIssues = result.getIssues().stream().map(IssueDto::toDefaultIssue).collect(MoreCollectors.toList(result.getIssues().size()));
issueChangePostProcessor.process(dbSession, changedIssues, singleton(component));
}

SearchResponseData preloadedSearchResponseData = new SearchResponseData(issueDto);
rule.ifPresent(r -> preloadedSearchResponseData.setRules(singletonList(r)));
preloadedSearchResponseData.addComponents(singleton(project));
preloadedSearchResponseData.addComponents(singleton(component));
return preloadedSearchResponseData;
return result;
}

public IssueDto saveIssue(DbSession session, DefaultIssue issue, IssueChangeContext context, @Nullable String comment) {
Optional<RuleDefinitionDto> rule = getRuleByKey(session, issue.getRuleKey());
ComponentDto project = dbClient.componentDao().selectOrFailByUuid(session, issue.projectUuid());
ComponentDto component = dbClient.componentDao().selectOrFailByUuid(session, issue.componentUuid());
return saveIssue(session, issue, context, comment, rule, project, component);
return doSaveIssue(session, issue, context, comment, rule, project, component);
}

private IssueDto saveIssue(DbSession session, DefaultIssue issue, IssueChangeContext context, @Nullable String comment,
private IssueDto doSaveIssue(DbSession session, DefaultIssue issue, IssueChangeContext context, @Nullable String comment,
Optional<RuleDefinitionDto> rule, ComponentDto project, ComponentDto component) {
IssueDto issueDto = issueStorage.save(session, issue);
notificationService.scheduleForSending(new IssueChangeNotification()
@@ -87,7 +99,7 @@ public class IssueUpdater {
}

private Optional<RuleDefinitionDto> getRuleByKey(DbSession session, RuleKey ruleKey) {
Optional<RuleDefinitionDto> rule = Optional.ofNullable(dbClient.ruleDao().selectDefinitionByKey(session, ruleKey).orElse(null));
Optional<RuleDefinitionDto> rule = dbClient.ruleDao().selectDefinitionByKey(session, ruleKey);
return (rule.isPresent() && rule.get().getStatus() != RuleStatus.REMOVED) ? rule : Optional.empty();
}


+ 5
- 0
server/sonar-server/src/main/java/org/sonar/server/issue/SetSeverityAction.java Visa fil

@@ -61,6 +61,11 @@ public class SetSeverityAction extends Action {
return issueUpdater.setManualSeverity(context.issue(), verifySeverityParameter(properties), context.issueChangeContext());
}

@Override
public boolean shouldRefreshMeasures() {
return true;
}

private static String verifySeverityParameter(Map<String, Object> properties) {
String param = (String) properties.get(SEVERITY_PARAMETER);
checkArgument(!isNullOrEmpty(param), "Missing parameter : '%s'", SEVERITY_PARAMETER);

+ 5
- 0
server/sonar-server/src/main/java/org/sonar/server/issue/SetTypeAction.java Visa fil

@@ -61,6 +61,11 @@ public class SetTypeAction extends Action {
return issueUpdater.setType(context.issue(), RuleType.valueOf(type), context.issueChangeContext());
}

@Override
public boolean shouldRefreshMeasures() {
return true;
}

private static String verifyTypeParameter(Map<String, Object> properties) {
String type = (String) properties.get(TYPE_PARAMETER);
checkArgument(!isNullOrEmpty(type), "Missing parameter : '%s'", TYPE_PARAMETER);

+ 5
- 0
server/sonar-server/src/main/java/org/sonar/server/issue/TransitionAction.java Visa fil

@@ -56,6 +56,11 @@ public class TransitionAction extends Action {
return canExecuteTransition(issue, transition) && transitionService.doTransition(context.issue(), context.issueChangeContext(), transition(properties));
}

@Override
public boolean shouldRefreshMeasures() {
return true;
}

private boolean canExecuteTransition(DefaultIssue issue, String transitionKey) {
return transitionService.listTransitions(issue)
.stream()

+ 2
- 1
server/sonar-server/src/main/java/org/sonar/server/issue/index/IssueIndexer.java Visa fil

@@ -108,10 +108,11 @@ public class IssueIndexer implements ProjectIndexer, NeedAuthorizationIndexer {
switch (cause) {
case PROJECT_CREATION:
// nothing to do, issues do not exist at project creation
case MEASURE_CHANGE:
case PROJECT_KEY_UPDATE:
case PROJECT_TAGS_UPDATE:
case PERMISSION_CHANGE:
// nothing to do, permissions, project key and tags are not used in type issues/issue
// nothing to do. Measures, permissions, project key and tags are not used in type issues/issue
return emptyList();

case PROJECT_DELETION:

+ 1
- 1
server/sonar-server/src/main/java/org/sonar/server/issue/ws/AddCommentAction.java Visa fil

@@ -98,7 +98,7 @@ public class AddCommentAction implements IssuesWsAction {
IssueChangeContext context = IssueChangeContext.createUser(new Date(system2.now()), userSession.getLogin());
DefaultIssue defaultIssue = issueDto.toDefaultIssue();
issueFieldsSetter.addComment(defaultIssue, wsRequest.getText(), context);
SearchResponseData preloadedSearchResponseData = issueUpdater.saveIssueAndPreloadSearchResponseData(dbSession, defaultIssue, context, wsRequest.getText());
SearchResponseData preloadedSearchResponseData = issueUpdater.saveIssueAndPreloadSearchResponseData(dbSession, defaultIssue, context, wsRequest.getText(), false);
responseWriter.write(defaultIssue.key(), preloadedSearchResponseData, request, response);
}
}

+ 2
- 2
server/sonar-server/src/main/java/org/sonar/server/issue/ws/AssignAction.java Visa fil

@@ -67,7 +67,7 @@ public class AssignAction implements IssuesWsAction {
private final OperationResponseWriter responseWriter;

public AssignAction(System2 system2, UserSession userSession, DbClient dbClient, IssueFinder issueFinder, IssueFieldsSetter issueFieldsSetter, IssueUpdater issueUpdater,
OperationResponseWriter responseWriter) {
OperationResponseWriter responseWriter) {
this.system2 = system2;
this.userSession = userSession;
this.dbClient = dbClient;
@@ -121,7 +121,7 @@ public class AssignAction implements IssuesWsAction {
}
IssueChangeContext context = IssueChangeContext.createUser(new Date(system2.now()), userSession.getLogin());
if (issueFieldsSetter.assign(issue, user, context)) {
return issueUpdater.saveIssueAndPreloadSearchResponseData(dbSession, issue, context, null);
return issueUpdater.saveIssueAndPreloadSearchResponseData(dbSession, issue, context, null, false);
}
return new SearchResponseData(issueDto);
}

+ 48
- 64
server/sonar-server/src/main/java/org/sonar/server/issue/ws/BulkChangeAction.java Visa fil

@@ -28,9 +28,8 @@ import java.util.Map;
import java.util.Optional;
import java.util.Set;
import java.util.function.Consumer;
import java.util.function.Function;
import java.util.function.Predicate;
import java.util.stream.Stream;
import java.util.stream.Collectors;
import org.sonar.api.issue.DefaultTransitions;
import org.sonar.api.rule.RuleKey;
import org.sonar.api.rule.Severity;
@@ -54,20 +53,15 @@ import org.sonar.db.rule.RuleDefinitionDto;
import org.sonar.server.issue.Action;
import org.sonar.server.issue.AddTagsAction;
import org.sonar.server.issue.AssignAction;
import org.sonar.server.issue.IssueChangePostProcessor;
import org.sonar.server.issue.IssueStorage;
import org.sonar.server.issue.RemoveTagsAction;
import org.sonar.server.issue.SetTypeAction;
import org.sonar.server.issue.TransitionAction;
import org.sonar.server.issue.notification.IssueChangeNotification;
import org.sonar.server.notification.NotificationManager;
import org.sonar.server.qualitygate.changeevent.QGChangeEvent;
import org.sonar.server.qualitygate.changeevent.QGChangeEventFactory;
import org.sonar.server.qualitygate.changeevent.QGChangeEventListeners;
import org.sonar.server.user.UserSession;
import org.sonarqube.ws.Issues;

import static com.google.common.base.Preconditions.checkArgument;
import static com.google.common.collect.ImmutableList.copyOf;
import static com.google.common.collect.ImmutableMap.of;
import static java.lang.String.format;
import static java.util.function.Function.identity;
@@ -111,20 +105,18 @@ public class BulkChangeAction implements IssuesWsAction {
private final IssueStorage issueStorage;
private final NotificationManager notificationService;
private final List<Action> actions;
private final QGChangeEventFactory qgChangeEventFactory;
private final QGChangeEventListeners qgChangeEventListeners;
private final IssueChangePostProcessor issueChangePostProcessor;

public BulkChangeAction(System2 system2, UserSession userSession, DbClient dbClient, IssueStorage issueStorage,
NotificationManager notificationService, List<Action> actions,
QGChangeEventFactory qgChangeEventFactory, QGChangeEventListeners qgChangeEventListeners) {
IssueChangePostProcessor issueChangePostProcessor) {
this.system2 = system2;
this.userSession = userSession;
this.dbClient = dbClient;
this.issueStorage = issueStorage;
this.notificationService = notificationService;
this.actions = actions;
this.qgChangeEventFactory = qgChangeEventFactory;
this.qgChangeEventListeners = qgChangeEventListeners;
this.issueChangePostProcessor = issueChangePostProcessor;
}

@Override
@@ -187,53 +179,41 @@ public class BulkChangeAction implements IssuesWsAction {
public void handle(Request request, Response response) throws Exception {
userSession.checkLoggedIn();
try (DbSession dbSession = dbClient.openSession(false)) {
Issues.BulkChangeWsResponse wsResponse = Stream.of(request)
.map(loadData(dbSession))
.map(executeBulkChange())
.map(toWsResponse())
.collect(MoreCollectors.toOneElement());
writeProtobuf(wsResponse, request, response);
BulkChangeResult result = executeBulkChange(dbSession, request);
writeProtobuf(toWsResponse(result), request, response);
}
}

private Function<Request, BulkChangeData> loadData(DbSession dbSession) {
return request -> new BulkChangeData(dbSession, request);
}
private BulkChangeResult executeBulkChange(DbSession dbSession, Request request) {
BulkChangeData bulkChangeData = new BulkChangeData(dbSession, request);
BulkChangeResult result = new BulkChangeResult(bulkChangeData.issues.size());
IssueChangeContext issueChangeContext = IssueChangeContext.createUser(new Date(system2.now()), userSession.getLogin());

private Function<BulkChangeData, BulkChangeResult> executeBulkChange() {
return bulkChangeData -> {
BulkChangeResult result = new BulkChangeResult(bulkChangeData.issues.size());
IssueChangeContext issueChangeContext = IssueChangeContext.createUser(new Date(system2.now()), userSession.getLogin());
List<DefaultIssue> items = bulkChangeData.issues.stream()
.filter(bulkChange(issueChangeContext, bulkChangeData, result))
.collect(MoreCollectors.toList());
issueStorage.save(items);

List<DefaultIssue> items = bulkChangeData.issues.stream()
.filter(bulkChange(issueChangeContext, bulkChangeData, result))
.collect(MoreCollectors.toList());
issueStorage.save(items);
items.forEach(sendNotification(issueChangeContext, bulkChangeData));
buildWebhookIssueChange(bulkChangeData.propertiesByActions)
.ifPresent(issueChange -> {
QGChangeEventFactory.IssueChangeData issueChangeData = new QGChangeEventFactory.IssueChangeData(
bulkChangeData.issues.stream().filter(i -> result.success.contains(i.key())).collect(MoreCollectors.toList()),
copyOf(bulkChangeData.componentsByUuid.values()));
List<QGChangeEvent> qgChangeEvents = qgChangeEventFactory.from(issueChangeData, issueChange, issueChangeContext);
qgChangeEventListeners.broadcastOnIssueChange(issueChangeData, qgChangeEvents);
});
return result;
};
refreshLiveMeasures(dbSession, bulkChangeData, result);

items.forEach(sendNotification(issueChangeContext, bulkChangeData));

return result;
}

private static Optional<QGChangeEventFactory.IssueChange> buildWebhookIssueChange(Map<String, Map<String, Object>> propertiesByActions) {
RuleType ruleType = Optional.ofNullable(propertiesByActions.get(SetTypeAction.SET_TYPE_KEY))
.map(t -> (String) t.get(SetTypeAction.TYPE_PARAMETER))
.map(RuleType::valueOf)
.orElse(null);
String transitionKey = Optional.ofNullable(propertiesByActions.get(TransitionAction.DO_TRANSITION_KEY))
.map(t -> (String) t.get(TransitionAction.TRANSITION_PARAMETER))
.orElse(null);
if (ruleType == null && transitionKey == null) {
return Optional.empty();
private void refreshLiveMeasures(DbSession dbSession, BulkChangeData data, BulkChangeResult result) {
if (!data.shouldRefreshMeasures()) {
return;
}
return Optional.of(new QGChangeEventFactory.IssueChange(ruleType, transitionKey));
Set<String> touchedComponentUuids = result.success.stream()
.map(DefaultIssue::componentUuid)
.collect(Collectors.toSet());
List<ComponentDto> touchedComponents = touchedComponentUuids.stream()
.map(data.componentsByUuid::get)
.collect(MoreCollectors.toList(touchedComponentUuids.size()));

List<DefaultIssue> changedIssues = data.issues.stream().filter(result.success::contains).collect(MoreCollectors.toList());
issueChangePostProcessor.process(dbSession, changedIssues, touchedComponents);
}

private static Predicate<DefaultIssue> bulkChange(IssueChangeContext issueChangeContext, BulkChangeData bulkChangeData, BulkChangeResult result) {
@@ -241,7 +221,7 @@ public class BulkChangeAction implements IssuesWsAction {
ActionContext actionContext = new ActionContext(issue, issueChangeContext, bulkChangeData.projectsByUuid.get(issue.projectUuid()));
bulkChangeData.getActionsWithoutComment().forEach(applyAction(actionContext, bulkChangeData, result));
addCommentIfNeeded(actionContext, bulkChangeData);
return result.success.contains(issue.key());
return result.success.contains(issue);
};
}

@@ -277,12 +257,12 @@ public class BulkChangeAction implements IssuesWsAction {
};
}

private static Function<BulkChangeResult, Issues.BulkChangeWsResponse> toWsResponse() {
return bulkChangeResult -> Issues.BulkChangeWsResponse.newBuilder()
.setTotal(bulkChangeResult.getTotal())
.setSuccess(bulkChangeResult.getSuccess())
.setIgnored((long) bulkChangeResult.getTotal() - (bulkChangeResult.getSuccess() + bulkChangeResult.getFailures()))
.setFailures(bulkChangeResult.getFailures())
private static Issues.BulkChangeWsResponse toWsResponse(BulkChangeResult result) {
return Issues.BulkChangeWsResponse.newBuilder()
.setTotal(result.countTotal())
.setSuccess(result.countSuccess())
.setIgnored((long) result.countTotal() - (result.countSuccess() + result.countFailures()))
.setFailures(result.countFailures())
.build();
}

@@ -391,11 +371,15 @@ public class BulkChangeAction implements IssuesWsAction {
long actionsDefined = actions.stream().filter(action -> !action.equals(COMMENT_KEY)).count();
checkArgument(actionsDefined > 0, "At least one action must be provided");
}

private boolean shouldRefreshMeasures() {
return availableActions.stream().anyMatch(Action::shouldRefreshMeasures);
}
}

private static class BulkChangeResult {
private final int total;
private Set<String> success = new HashSet<>();
private final Set<DefaultIssue> success = new HashSet<>();
private int failures = 0;

BulkChangeResult(int total) {
@@ -403,22 +387,22 @@ public class BulkChangeAction implements IssuesWsAction {
}

void increaseSuccess(DefaultIssue issue) {
this.success.add(issue.key());
this.success.add(issue);
}

void increaseFailure() {
this.failures++;
}

public int getTotal() {
int countTotal() {
return total;
}

public int getSuccess() {
int countSuccess() {
return success.size();
}

public int getFailures() {
int countFailures() {
return failures;
}
}

+ 2
- 18
server/sonar-server/src/main/java/org/sonar/server/issue/ws/DoTransitionAction.java Visa fil

@@ -21,7 +21,6 @@ package org.sonar.server.issue.ws;

import com.google.common.io.Resources;
import java.util.Date;
import java.util.List;
import org.sonar.api.issue.DefaultTransitions;
import org.sonar.api.server.ws.Change;
import org.sonar.api.server.ws.Request;
@@ -37,13 +36,8 @@ import org.sonar.db.issue.IssueDto;
import org.sonar.server.issue.IssueFinder;
import org.sonar.server.issue.IssueUpdater;
import org.sonar.server.issue.TransitionService;
import org.sonar.server.qualitygate.changeevent.QGChangeEvent;
import org.sonar.server.qualitygate.changeevent.QGChangeEventFactory;
import org.sonar.server.qualitygate.changeevent.QGChangeEventListeners;
import org.sonar.server.user.UserSession;

import static com.google.common.collect.ImmutableList.copyOf;
import static org.sonar.core.util.stream.MoreCollectors.toList;
import static org.sonarqube.ws.client.issue.IssuesWsParameters.ACTION_DO_TRANSITION;
import static org.sonarqube.ws.client.issue.IssuesWsParameters.PARAM_ISSUE;
import static org.sonarqube.ws.client.issue.IssuesWsParameters.PARAM_TRANSITION;
@@ -57,11 +51,9 @@ public class DoTransitionAction implements IssuesWsAction {
private final TransitionService transitionService;
private final OperationResponseWriter responseWriter;
private final System2 system2;
private final QGChangeEventFactory qgChangeEventFactory;
private final QGChangeEventListeners qgChangeEventListeners;

public DoTransitionAction(DbClient dbClient, UserSession userSession, IssueFinder issueFinder, IssueUpdater issueUpdater, TransitionService transitionService,
OperationResponseWriter responseWriter, System2 system2, QGChangeEventFactory qgChangeEventFactory, QGChangeEventListeners qgChangeEventListeners) {
OperationResponseWriter responseWriter, System2 system2) {
this.dbClient = dbClient;
this.userSession = userSession;
this.issueFinder = issueFinder;
@@ -69,8 +61,6 @@ public class DoTransitionAction implements IssuesWsAction {
this.transitionService = transitionService;
this.responseWriter = responseWriter;
this.system2 = system2;
this.qgChangeEventFactory = qgChangeEventFactory;
this.qgChangeEventListeners = qgChangeEventListeners;
}

@Override
@@ -112,13 +102,7 @@ public class DoTransitionAction implements IssuesWsAction {
IssueChangeContext context = IssueChangeContext.createUser(new Date(system2.now()), userSession.getLogin());
transitionService.checkTransitionPermission(transitionKey, defaultIssue);
if (transitionService.doTransition(defaultIssue, context, transitionKey)) {
SearchResponseData searchResponseData = issueUpdater.saveIssueAndPreloadSearchResponseData(session, defaultIssue, context, null);
QGChangeEventFactory.IssueChangeData issueChangeData = new QGChangeEventFactory.IssueChangeData(
searchResponseData.getIssues().stream().map(IssueDto::toDefaultIssue).collect(toList(searchResponseData.getIssues().size())),
copyOf(searchResponseData.getComponents()));
List<QGChangeEvent> qgChangeEvents = qgChangeEventFactory.from(issueChangeData, new QGChangeEventFactory.IssueChange(transitionKey), context);
qgChangeEventListeners.broadcastOnIssueChange(issueChangeData, qgChangeEvents);
return searchResponseData;
return issueUpdater.saveIssueAndPreloadSearchResponseData(session, defaultIssue, context, null, true);
}
return new SearchResponseData(issueDto);
}

+ 0
- 4
server/sonar-server/src/main/java/org/sonar/server/issue/ws/IssueWsModule.java Visa fil

@@ -28,8 +28,6 @@ import org.sonar.server.issue.ServerIssueStorage;
import org.sonar.server.issue.TransitionService;
import org.sonar.server.issue.workflow.FunctionExecutor;
import org.sonar.server.issue.workflow.IssueWorkflow;
import org.sonar.server.qualitygate.LiveQualityGateFactoryImpl;
import org.sonar.server.qualitygate.changeevent.QGChangeEventFactoryImpl;
import org.sonar.server.qualitygate.changeevent.QGChangeEventListenersImpl;
import org.sonar.server.settings.ProjectConfigurationLoaderImpl;
import org.sonar.server.webhook.WebhookQGChangeEventListener;
@@ -68,8 +66,6 @@ public class IssueWsModule extends Module {
ChangelogAction.class,
BulkChangeAction.class,
ProjectConfigurationLoaderImpl.class,
LiveQualityGateFactoryImpl.class,
QGChangeEventFactoryImpl.class,
WebhookQGChangeEventListener.class,
QGChangeEventListenersImpl.class);
}

+ 1
- 1
server/sonar-server/src/main/java/org/sonar/server/issue/ws/SetSeverityAction.java Visa fil

@@ -107,7 +107,7 @@ public class SetSeverityAction implements IssuesWsAction {

IssueChangeContext context = IssueChangeContext.createUser(new Date(), userSession.getLogin());
if (issueFieldsSetter.setManualSeverity(issue, severity, context)) {
return issueUpdater.saveIssueAndPreloadSearchResponseData(session, issue, context, null);
return issueUpdater.saveIssueAndPreloadSearchResponseData(session, issue, context, null, true);
}
return new SearchResponseData(issueDto);
}

+ 1
- 1
server/sonar-server/src/main/java/org/sonar/server/issue/ws/SetTagsAction.java Visa fil

@@ -105,7 +105,7 @@ public class SetTagsAction implements IssuesWsAction {
DefaultIssue issue = issueDto.toDefaultIssue();
IssueChangeContext context = IssueChangeContext.createUser(new Date(), userSession.getLogin());
if (issueFieldsSetter.setTags(issue, tags, context)) {
return issueUpdater.saveIssueAndPreloadSearchResponseData(session, issue, context, null);
return issueUpdater.saveIssueAndPreloadSearchResponseData(session, issue, context, null, false);
}
return new SearchResponseData(issueDto);
}

+ 2
- 19
server/sonar-server/src/main/java/org/sonar/server/issue/ws/SetTypeAction.java Visa fil

@@ -21,7 +21,6 @@ package org.sonar.server.issue.ws;

import com.google.common.io.Resources;
import java.util.Date;
import java.util.List;
import org.sonar.api.rules.RuleType;
import org.sonar.api.server.ws.Change;
import org.sonar.api.server.ws.Request;
@@ -31,19 +30,14 @@ import org.sonar.api.utils.System2;
import org.sonar.core.issue.DefaultIssue;
import org.sonar.core.issue.IssueChangeContext;
import org.sonar.core.util.Uuids;
import org.sonar.core.util.stream.MoreCollectors;
import org.sonar.db.DbClient;
import org.sonar.db.DbSession;
import org.sonar.db.issue.IssueDto;
import org.sonar.server.issue.IssueFieldsSetter;
import org.sonar.server.issue.IssueFinder;
import org.sonar.server.issue.IssueUpdater;
import org.sonar.server.qualitygate.changeevent.QGChangeEvent;
import org.sonar.server.qualitygate.changeevent.QGChangeEventFactory;
import org.sonar.server.qualitygate.changeevent.QGChangeEventListeners;
import org.sonar.server.user.UserSession;

import static com.google.common.collect.ImmutableList.copyOf;
import static org.sonar.api.web.UserRole.ISSUE_ADMIN;
import static org.sonarqube.ws.client.issue.IssuesWsParameters.ACTION_SET_TYPE;
import static org.sonarqube.ws.client.issue.IssuesWsParameters.PARAM_ISSUE;
@@ -58,12 +52,9 @@ public class SetTypeAction implements IssuesWsAction {
private final IssueUpdater issueUpdater;
private final OperationResponseWriter responseWriter;
private final System2 system2;
private final QGChangeEventFactory qgChangeEventFactory;
private final QGChangeEventListeners qgChangeEventListeners;

public SetTypeAction(UserSession userSession, DbClient dbClient, IssueFinder issueFinder, IssueFieldsSetter issueFieldsSetter, IssueUpdater issueUpdater,
OperationResponseWriter responseWriter, System2 system2,
QGChangeEventFactory qgChangeEventFactory, QGChangeEventListeners qgChangeEventListeners) {
OperationResponseWriter responseWriter, System2 system2) {
this.userSession = userSession;
this.dbClient = dbClient;
this.issueFinder = issueFinder;
@@ -71,8 +62,6 @@ public class SetTypeAction implements IssuesWsAction {
this.issueUpdater = issueUpdater;
this.responseWriter = responseWriter;
this.system2 = system2;
this.qgChangeEventFactory = qgChangeEventFactory;
this.qgChangeEventListeners = qgChangeEventListeners;
}

@Override
@@ -121,13 +110,7 @@ public class SetTypeAction implements IssuesWsAction {

IssueChangeContext context = IssueChangeContext.createUser(new Date(system2.now()), userSession.getLogin());
if (issueFieldsSetter.setType(issue, ruleType, context)) {
SearchResponseData searchResponseData = issueUpdater.saveIssueAndPreloadSearchResponseData(session, issue, context, null);
QGChangeEventFactory.IssueChangeData issueChangeData = new QGChangeEventFactory.IssueChangeData(
searchResponseData.getIssues().stream().map(IssueDto::toDefaultIssue).collect(MoreCollectors.toList(searchResponseData.getIssues().size())),
copyOf(searchResponseData.getComponents()));
List<QGChangeEvent> qgChangeEvents = qgChangeEventFactory.from(issueChangeData, new QGChangeEventFactory.IssueChange(ruleType), context);
qgChangeEventListeners.broadcastOnIssueChange(issueChangeData, qgChangeEvents);
return searchResponseData;
return issueUpdater.saveIssueAndPreloadSearchResponseData(session, issue, context, null, true);
}
return new SearchResponseData(issueDto);
}

+ 14
- 3
server/sonar-server/src/main/java/org/sonar/server/measure/index/ProjectMeasuresIndexer.java Visa fil

@@ -78,8 +78,8 @@ public class ProjectMeasuresIndexer implements ProjectIndexer, NeedAuthorization
}

@Override
public void indexOnAnalysis(String branchUuid) {
doIndex(Size.REGULAR, branchUuid);
public void indexOnAnalysis(String projectUuid) {
doIndex(Size.REGULAR, projectUuid);
}

@Override
@@ -88,7 +88,7 @@ public class ProjectMeasuresIndexer implements ProjectIndexer, NeedAuthorization
case PERMISSION_CHANGE:
// nothing to do, permissions are not used in type projectmeasures/projectmeasure
return Collections.emptyList();
case MEASURE_CHANGE:
case PROJECT_KEY_UPDATE:
// project must be re-indexed because key is used in this index
case PROJECT_CREATION:
@@ -106,6 +106,17 @@ public class ProjectMeasuresIndexer implements ProjectIndexer, NeedAuthorization
}
}

public IndexingResult commitAndIndex(DbSession dbSession, Collection<String> projectUuids) {
List<EsQueueDto> items = projectUuids.stream()
.map(projectUuid -> EsQueueDto.create(INDEX_TYPE_PROJECT_MEASURES.format(), projectUuid, null, projectUuid))
.collect(MoreCollectors.toArrayList(projectUuids.size()));
dbClient.esQueueDao().insert(dbSession, items);

dbSession.commit();

return index(dbSession, items);
}

@Override
public IndexingResult index(DbSession dbSession, Collection<EsQueueDto> items) {
if (items.isEmpty()) {

+ 153
- 0
server/sonar-server/src/main/java/org/sonar/server/measure/live/IssueCounter.java Visa fil

@@ -0,0 +1,153 @@
/*
* 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.measure.live;

import java.util.Collection;
import java.util.EnumMap;
import java.util.HashMap;
import java.util.Map;
import java.util.Optional;
import javax.annotation.Nullable;
import org.sonar.api.rule.Severity;
import org.sonar.api.rules.RuleType;
import org.sonar.db.issue.IssueGroupDto;
import org.sonar.db.rule.SeverityUtil;

class IssueCounter {

private final Map<RuleType, HighestSeverity> highestSeverityOfUnresolved = new EnumMap<>(RuleType.class);
private final Map<RuleType, Effort> effortOfUnresolved = new EnumMap<>(RuleType.class);
private final Map<String, Count> unresolvedBySeverity = new HashMap<>();
private final Map<RuleType, Count> unresolvedByType = new EnumMap<>(RuleType.class);
private final Map<String, Count> byResolution = new HashMap<>();
private final Map<String, Count> byStatus = new HashMap<>();
private final Count unresolved = new Count();

IssueCounter(Collection<IssueGroupDto> groups) {
for (IssueGroupDto group : groups) {
RuleType ruleType = RuleType.valueOf(group.getRuleType());
if (group.getResolution() == null) {
highestSeverityOfUnresolved
.computeIfAbsent(ruleType, k -> new HighestSeverity())
.add(group);
effortOfUnresolved
.computeIfAbsent(ruleType, k -> new Effort())
.add(group);
unresolvedBySeverity
.computeIfAbsent(group.getSeverity(), k -> new Count())
.add(group);
unresolvedByType
.computeIfAbsent(ruleType, k -> new Count())
.add(group);
unresolved.add(group);
} else {
byResolution
.computeIfAbsent(group.getResolution(), k -> new Count())
.add(group);
}
if (group.getStatus() != null) {
byStatus
.computeIfAbsent(group.getStatus(), k -> new Count())
.add(group);
}
}
}

public Optional<String> getHighestSeverityOfUnresolved(RuleType ruleType, boolean onlyInLeak) {
return Optional.ofNullable(highestSeverityOfUnresolved.get(ruleType))
.map(hs -> hs.severity(onlyInLeak));
}

public double sumEffortOfUnresolved(RuleType type, boolean onlyInLeak) {
Effort effort = effortOfUnresolved.get(type);
if (effort == null) {
return 0.0;
}
return onlyInLeak ? effort.leak : effort.absolute;
}

public long countUnresolvedBySeverity(String severity, boolean onlyInLeak) {
return value(unresolvedBySeverity.get(severity), onlyInLeak);
}

public long countByResolution(String resolution, boolean onlyInLeak) {
return value(byResolution.get(resolution), onlyInLeak);
}

public long countUnresolvedByType(RuleType type, boolean onlyInLeak) {
return value(unresolvedByType.get(type), onlyInLeak);
}

public long countByStatus(String status, boolean onlyInLeak) {
return value(byStatus.get(status), onlyInLeak);
}

public long countUnresolved(boolean onlyInLeak) {
return value(unresolved, onlyInLeak);
}

private static long value(@Nullable Count count, boolean onlyInLeak) {
if (count == null) {
return 0;
}
return onlyInLeak ? count.leak : count.absolute;
}

private static class Count {
private long absolute = 0L;
private long leak = 0L;

void add(IssueGroupDto group) {
absolute += group.getCount();
if (group.isInLeak()) {
leak += group.getCount();
}
}
}

private static class Effort {
private double absolute = 0.0;
private double leak = 0.0;

void add(IssueGroupDto group) {
absolute += group.getEffort();
if (group.isInLeak()) {
leak += group.getEffort();
}
}
}

private static class HighestSeverity {
private int absolute = SeverityUtil.getOrdinalFromSeverity(Severity.INFO);
private int leak = SeverityUtil.getOrdinalFromSeverity(Severity.INFO);

void add(IssueGroupDto group) {
int severity = SeverityUtil.getOrdinalFromSeverity(group.getSeverity());
absolute = Math.max(severity, absolute);
if (group.isInLeak()) {
leak = Math.max(severity, leak);
}
}

String severity(boolean inLeak) {
return SeverityUtil.getSeverityFromOrdinal(inLeak ? leak : absolute);
}
}
}

+ 89
- 0
server/sonar-server/src/main/java/org/sonar/server/measure/live/IssueMetricFormula.java Visa fil

@@ -0,0 +1,89 @@
/*
* 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.measure.live;

import java.util.Collection;
import java.util.Optional;
import java.util.function.BiConsumer;
import org.sonar.api.measures.Metric;
import org.sonar.db.component.ComponentDto;
import org.sonar.server.computation.task.projectanalysis.qualitymodel.DebtRatingGrid;
import org.sonar.server.computation.task.projectanalysis.qualitymodel.Rating;

import static java.util.Collections.emptyList;

class IssueMetricFormula {

private final Metric metric;
private final boolean onLeak;
private final BiConsumer<Context, IssueCounter> formula;
private final Collection<Metric> dependentMetrics;

IssueMetricFormula(Metric metric, boolean onLeak, BiConsumer<Context, IssueCounter> formula) {
this(metric, onLeak, formula, emptyList());
}

IssueMetricFormula(Metric metric, boolean onLeak, BiConsumer<Context, IssueCounter> formula, Collection<Metric> dependentMetrics) {
this.metric = metric;
this.onLeak = onLeak;
this.formula = formula;
this.dependentMetrics = dependentMetrics;
}

Metric getMetric() {
return metric;
}

boolean isOnLeak() {
return onLeak;
}

Collection<Metric> getDependentMetrics() {
return dependentMetrics;
}

void compute(Context context, IssueCounter issues) {
formula.accept(context, issues);
}

interface Context {
ComponentDto getComponent();

DebtRatingGrid getDebtRatingGrid();

/**
* Value that was just refreshed, otherwise value as computed
* during last analysis.
* The metric must be declared in the formula dependencies
* (see {@link IssueMetricFormula#getDependentMetrics()}).
*/
Optional<Double> getValue(Metric metric);

Optional<Double> getLeakValue(Metric metric);

void setValue(double value);

void setValue(Rating value);

void setLeakValue(double value);

void setLeakValue(Rating value);
}
}

+ 40
- 0
server/sonar-server/src/main/java/org/sonar/server/measure/live/IssueMetricFormulaFactory.java Visa fil

@@ -0,0 +1,40 @@
/*
* 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.measure.live;

import java.util.List;
import java.util.Set;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import org.sonar.api.measures.Metric;
import org.sonar.api.server.ServerSide;

@ServerSide
public interface IssueMetricFormulaFactory {
List<IssueMetricFormula> getFormulas();

Set<Metric> getFormulaMetrics();

static Set<Metric> extractMetrics(List<IssueMetricFormula> formulas) {
return formulas.stream()
.flatMap(f -> Stream.concat(Stream.of(f.getMetric()), f.getDependentMetrics().stream()))
.collect(Collectors.toSet());
}
}

+ 200
- 0
server/sonar-server/src/main/java/org/sonar/server/measure/live/IssueMetricFormulaFactoryImpl.java Visa fil

@@ -0,0 +1,200 @@
/*
* 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.measure.live;

import java.util.List;
import java.util.Optional;
import java.util.Set;
import org.sonar.api.issue.Issue;
import org.sonar.api.measures.CoreMetrics;
import org.sonar.api.measures.Metric;
import org.sonar.api.rule.Severity;
import org.sonar.api.rules.RuleType;
import org.sonar.server.computation.task.projectanalysis.qualitymodel.Rating;

import static java.util.Arrays.asList;
import static org.sonar.server.computation.task.projectanalysis.qualitymodel.Rating.RATING_BY_SEVERITY;

public class IssueMetricFormulaFactoryImpl implements IssueMetricFormulaFactory {

private static final List<IssueMetricFormula> FORMULAS = asList(
new IssueMetricFormula(CoreMetrics.CODE_SMELLS, false,
(context, issues) -> context.setValue(issues.countUnresolvedByType(RuleType.CODE_SMELL, false))),

new IssueMetricFormula(CoreMetrics.BUGS, false,
(context, issues) -> context.setValue(issues.countUnresolvedByType(RuleType.BUG, false))),

new IssueMetricFormula(CoreMetrics.VULNERABILITIES, false,
(context, issues) -> context.setValue(issues.countUnresolvedByType(RuleType.VULNERABILITY, false))),

new IssueMetricFormula(CoreMetrics.VIOLATIONS, false,
(context, issues) -> context.setValue(issues.countUnresolved(false))),

new IssueMetricFormula(CoreMetrics.BLOCKER_VIOLATIONS, false,
(context, issues) -> context.setValue(issues.countUnresolvedBySeverity(Severity.BLOCKER, false))),

new IssueMetricFormula(CoreMetrics.CRITICAL_VIOLATIONS, false,
(context, issues) -> context.setValue(issues.countUnresolvedBySeverity(Severity.CRITICAL, false))),

new IssueMetricFormula(CoreMetrics.MAJOR_VIOLATIONS, false,
(context, issues) -> context.setValue(issues.countUnresolvedBySeverity(Severity.MAJOR, false))),

new IssueMetricFormula(CoreMetrics.MINOR_VIOLATIONS, false,
(context, issues) -> context.setValue(issues.countUnresolvedBySeverity(Severity.MINOR, false))),

new IssueMetricFormula(CoreMetrics.INFO_VIOLATIONS, false,
(context, issues) -> context.setValue(issues.countUnresolvedBySeverity(Severity.INFO, false))),

new IssueMetricFormula(CoreMetrics.FALSE_POSITIVE_ISSUES, false,
(context, issues) -> context.setValue(issues.countByResolution(Issue.RESOLUTION_FALSE_POSITIVE, false))),

new IssueMetricFormula(CoreMetrics.WONT_FIX_ISSUES, false,
(context, issues) -> context.setValue(issues.countByResolution(Issue.RESOLUTION_WONT_FIX, false))),

new IssueMetricFormula(CoreMetrics.OPEN_ISSUES, false,
(context, issues) -> context.setValue(issues.countByStatus(Issue.STATUS_OPEN, false))),

new IssueMetricFormula(CoreMetrics.REOPENED_ISSUES, false,
(context, issues) -> context.setValue(issues.countByStatus(Issue.STATUS_REOPENED, false))),

new IssueMetricFormula(CoreMetrics.CONFIRMED_ISSUES, false,
(context, issues) -> context.setValue(issues.countByStatus(Issue.STATUS_CONFIRMED, false))),

new IssueMetricFormula(CoreMetrics.TECHNICAL_DEBT, false,
(context, issues) -> context.setValue(issues.sumEffortOfUnresolved(RuleType.CODE_SMELL, false))),

new IssueMetricFormula(CoreMetrics.RELIABILITY_REMEDIATION_EFFORT, false,
(context, issues) -> context.setValue(issues.sumEffortOfUnresolved(RuleType.BUG, false))),

new IssueMetricFormula(CoreMetrics.SECURITY_REMEDIATION_EFFORT, false,
(context, issues) -> context.setValue(issues.sumEffortOfUnresolved(RuleType.VULNERABILITY, false))),

new IssueMetricFormula(CoreMetrics.SQALE_DEBT_RATIO, false,
(context, issues) -> context.setValue(100.0 * debtDensity(context)),
asList(CoreMetrics.TECHNICAL_DEBT, CoreMetrics.DEVELOPMENT_COST)),

new IssueMetricFormula(CoreMetrics.SQALE_RATING, false,
(context, issues) -> context
.setValue(context.getDebtRatingGrid().getRatingForDensity(debtDensity(context))),
asList(CoreMetrics.TECHNICAL_DEBT, CoreMetrics.DEVELOPMENT_COST)),

new IssueMetricFormula(CoreMetrics.EFFORT_TO_REACH_MAINTAINABILITY_RATING_A, false,
(context, issues) -> context.setValue(effortToReachMaintainabilityRatingA(context)), asList(CoreMetrics.TECHNICAL_DEBT, CoreMetrics.DEVELOPMENT_COST)),

new IssueMetricFormula(CoreMetrics.RELIABILITY_RATING, false,
(context, issues) -> context.setValue(RATING_BY_SEVERITY.get(issues.getHighestSeverityOfUnresolved(RuleType.BUG, false).orElse(Severity.INFO)))),

new IssueMetricFormula(CoreMetrics.SECURITY_RATING, false,
(context, issues) -> context.setValue(RATING_BY_SEVERITY.get(issues.getHighestSeverityOfUnresolved(RuleType.VULNERABILITY, false).orElse(Severity.INFO)))),

new IssueMetricFormula(CoreMetrics.NEW_CODE_SMELLS, true,
(context, issues) -> context.setLeakValue(issues.countUnresolvedByType(RuleType.CODE_SMELL, true))),

new IssueMetricFormula(CoreMetrics.NEW_BUGS, true,
(context, issues) -> context.setLeakValue(issues.countUnresolvedByType(RuleType.BUG, true))),

new IssueMetricFormula(CoreMetrics.NEW_VULNERABILITIES, true,
(context, issues) -> context.setLeakValue(issues.countUnresolvedByType(RuleType.VULNERABILITY, true))),

new IssueMetricFormula(CoreMetrics.NEW_VIOLATIONS, true,
(context, issues) -> context.setLeakValue(issues.countUnresolved(true))),

new IssueMetricFormula(CoreMetrics.NEW_BLOCKER_VIOLATIONS, true,
(context, issues) -> context.setLeakValue(issues.countUnresolvedBySeverity(Severity.BLOCKER, true))),

new IssueMetricFormula(CoreMetrics.NEW_CRITICAL_VIOLATIONS, true,
(context, issues) -> context.setLeakValue(issues.countUnresolvedBySeverity(Severity.CRITICAL, true))),

new IssueMetricFormula(CoreMetrics.NEW_MAJOR_VIOLATIONS, true,
(context, issues) -> context.setLeakValue(issues.countUnresolvedBySeverity(Severity.MAJOR, true))),

new IssueMetricFormula(CoreMetrics.NEW_MINOR_VIOLATIONS, true,
(context, issues) -> context.setLeakValue(issues.countUnresolvedBySeverity(Severity.MINOR, true))),

new IssueMetricFormula(CoreMetrics.NEW_INFO_VIOLATIONS, true,
(context, issues) -> context.setLeakValue(issues.countUnresolvedBySeverity(Severity.INFO, true))),

new IssueMetricFormula(CoreMetrics.NEW_TECHNICAL_DEBT, true,
(context, issues) -> context.setLeakValue(issues.sumEffortOfUnresolved(RuleType.CODE_SMELL, true))),

new IssueMetricFormula(CoreMetrics.NEW_RELIABILITY_REMEDIATION_EFFORT, true,
(context, issues) -> context.setLeakValue(issues.sumEffortOfUnresolved(RuleType.BUG, true))),

new IssueMetricFormula(CoreMetrics.NEW_SECURITY_REMEDIATION_EFFORT, true,
(context, issues) -> context.setLeakValue(issues.sumEffortOfUnresolved(RuleType.VULNERABILITY, true))),

new IssueMetricFormula(CoreMetrics.NEW_RELIABILITY_RATING, true,
(context, issues) -> {
String highestSeverity = issues.getHighestSeverityOfUnresolved(RuleType.BUG, true).orElse(Severity.INFO);
context.setLeakValue(RATING_BY_SEVERITY.get(highestSeverity));
}),

new IssueMetricFormula(CoreMetrics.NEW_SECURITY_RATING, true,
(context, issues) -> {
String highestSeverity = issues.getHighestSeverityOfUnresolved(RuleType.VULNERABILITY, true).orElse(Severity.INFO);
context.setLeakValue(RATING_BY_SEVERITY.get(highestSeverity));
}),

new IssueMetricFormula(CoreMetrics.NEW_SQALE_DEBT_RATIO, true,
(context, issues) -> context.setLeakValue(100.0 * newDebtDensity(context)),
asList(CoreMetrics.NEW_TECHNICAL_DEBT, CoreMetrics.NEW_DEVELOPMENT_COST)),

new IssueMetricFormula(CoreMetrics.NEW_MAINTAINABILITY_RATING, true,
(context, issues) -> context.setLeakValue(context.getDebtRatingGrid().getRatingForDensity(
newDebtDensity(context))),
asList(CoreMetrics.NEW_TECHNICAL_DEBT, CoreMetrics.NEW_DEVELOPMENT_COST)));

private static final Set<Metric> FORMULA_METRICS = IssueMetricFormulaFactory.extractMetrics(FORMULAS);

private static double debtDensity(IssueMetricFormula.Context context) {
double debt = Math.max(context.getValue(CoreMetrics.TECHNICAL_DEBT).orElse(0.0), 0.0);
Optional<Double> devCost = context.getValue(CoreMetrics.DEVELOPMENT_COST);
if (devCost.isPresent() && Double.doubleToRawLongBits(devCost.get()) > 0L) {
return debt / devCost.get();
}
return 0d;
}

private static double newDebtDensity(IssueMetricFormula.Context context) {
double debt = Math.max(context.getLeakValue(CoreMetrics.NEW_TECHNICAL_DEBT).orElse(0.0), 0.0);
Optional<Double> devCost = context.getLeakValue(CoreMetrics.NEW_DEVELOPMENT_COST);
if (devCost.isPresent() && Double.doubleToRawLongBits(devCost.get()) > 0L) {
return debt / devCost.get();
}
return 0d;
}

private static double effortToReachMaintainabilityRatingA(IssueMetricFormula.Context context) {
double developmentCost = context.getValue(CoreMetrics.DEVELOPMENT_COST).orElse(0.0);
double effort = context.getValue(CoreMetrics.TECHNICAL_DEBT).orElse(0.0);
double upperGradeCost = context.getDebtRatingGrid().getGradeLowerBound(Rating.B) * developmentCost;
return upperGradeCost < effort ? (effort - upperGradeCost) : 0.0;
}

@Override
public List<IssueMetricFormula> getFormulas() {
return FORMULAS;
}

@Override
public Set<Metric> getFormulaMetrics() {
return FORMULA_METRICS;
}
}

+ 43
- 0
server/sonar-server/src/main/java/org/sonar/server/measure/live/LiveMeasureComputer.java Visa fil

@@ -0,0 +1,43 @@
/*
* 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.measure.live;

import java.util.Collection;
import java.util.List;
import org.sonar.api.server.ServerSide;
import org.sonar.db.DbSession;
import org.sonar.db.component.ComponentDto;
import org.sonar.server.qualitygate.changeevent.QGChangeEvent;

/**
* Refresh and persist the measures of some files, directories, modules
* or projects. Measures include status of quality gate.
*
* Touching a file updates the related directory, module and project.
* Status of Quality gate is refreshed but webhooks are not triggered.
*
* Branches are supported.
*/
@ServerSide
public interface LiveMeasureComputer {

List<QGChangeEvent> refresh(DbSession dbSession, Collection<ComponentDto> components);

}

+ 242
- 0
server/sonar-server/src/main/java/org/sonar/server/measure/live/LiveMeasureComputerImpl.java Visa fil

@@ -0,0 +1,242 @@
/*
* 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.measure.live;

import java.util.ArrayList;
import java.util.Collection;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
import org.sonar.api.config.Configuration;
import org.sonar.api.measures.Metric;
import org.sonar.db.DbClient;
import org.sonar.db.DbSession;
import org.sonar.db.component.BranchDto;
import org.sonar.db.component.ComponentDto;
import org.sonar.db.component.SnapshotDto;
import org.sonar.db.measure.LiveMeasureDto;
import org.sonar.db.metric.MetricDto;
import org.sonar.db.organization.OrganizationDto;
import org.sonar.server.computation.task.projectanalysis.qualitymodel.DebtRatingGrid;
import org.sonar.server.computation.task.projectanalysis.qualitymodel.Rating;
import org.sonar.server.es.ProjectIndexer;
import org.sonar.server.es.ProjectIndexers;
import org.sonar.server.qualitygate.EvaluatedQualityGate;
import org.sonar.server.qualitygate.QualityGate;
import org.sonar.server.qualitygate.changeevent.QGChangeEvent;
import org.sonar.server.settings.ProjectConfigurationLoader;

import static com.google.common.base.Preconditions.checkState;
import static java.util.Collections.emptyList;
import static java.util.Collections.singleton;
import static java.util.stream.Collectors.groupingBy;
import static org.sonar.core.util.stream.MoreCollectors.toArrayList;
import static org.sonar.core.util.stream.MoreCollectors.uniqueIndex;

public class LiveMeasureComputerImpl implements LiveMeasureComputer {

private final DbClient dbClient;
private final IssueMetricFormulaFactory formulaFactory;
private final LiveQualityGateComputer qGateComputer;
private final ProjectConfigurationLoader projectConfigurationLoader;
private final ProjectIndexers projectIndexer;

public LiveMeasureComputerImpl(DbClient dbClient, IssueMetricFormulaFactory formulaFactory,
LiveQualityGateComputer qGateComputer, ProjectConfigurationLoader projectConfigurationLoader, ProjectIndexers projectIndexer) {
this.dbClient = dbClient;
this.formulaFactory = formulaFactory;
this.qGateComputer = qGateComputer;
this.projectConfigurationLoader = projectConfigurationLoader;
this.projectIndexer = projectIndexer;
}

@Override
public List<QGChangeEvent> refresh(DbSession dbSession, Collection<ComponentDto> components) {
if (components.isEmpty()) {
return emptyList();
}

List<QGChangeEvent> result = new ArrayList<>();
Map<String, List<ComponentDto>> componentsByProjectUuid = components.stream().collect(groupingBy(ComponentDto::projectUuid));
for (List<ComponentDto> groupedComponents : componentsByProjectUuid.values()) {
Optional<QGChangeEvent> qgChangeEvent = refreshComponentsOnSameProject(dbSession, groupedComponents);
qgChangeEvent.ifPresent(result::add);
}
return result;
}

private Optional<QGChangeEvent> refreshComponentsOnSameProject(DbSession dbSession, List<ComponentDto> touchedComponents) {
// load all the components to be refreshed, including their ancestors
List<ComponentDto> components = loadTreeOfComponents(dbSession, touchedComponents);
ComponentDto project = findProject(components);
OrganizationDto organization = loadOrganization(dbSession, project);
BranchDto branch = loadBranch(dbSession, project);

Optional<SnapshotDto> lastAnalysis = dbClient.snapshotDao().selectLastAnalysisByRootComponentUuid(dbSession, project.uuid());
if (!lastAnalysis.isPresent()) {
return Optional.empty();
}
Optional<Long> beginningOfLeakPeriod = lastAnalysis.map(SnapshotDto::getPeriodDate);

QualityGate qualityGate = qGateComputer.loadQualityGate(dbSession, organization, project, branch);
Collection<String> metricKeys = getKeysOfAllInvolvedMetrics(qualityGate);

Map<Integer, MetricDto> metricsPerId = dbClient.metricDao().selectByKeys(dbSession, metricKeys).stream()
.collect(uniqueIndex(MetricDto::getId));
List<String> componentUuids = components.stream().map(ComponentDto::uuid).collect(toArrayList(components.size()));
List<LiveMeasureDto> dbMeasures = dbClient.liveMeasureDao().selectByComponentUuidsAndMetricIds(dbSession, componentUuids, metricsPerId.keySet());

Configuration config = projectConfigurationLoader.loadProjectConfiguration(dbSession, project);
DebtRatingGrid debtRatingGrid = new DebtRatingGrid(config);

MeasureMatrix matrix = new MeasureMatrix(components, metricsPerId.values(), dbMeasures);
components.forEach(c -> {
IssueCounter issueCounter = new IssueCounter(dbClient.issueDao().selectIssueGroupsByBaseComponent(dbSession, c, beginningOfLeakPeriod.orElse(Long.MAX_VALUE)));
FormulaContextImpl context = new FormulaContextImpl(matrix, debtRatingGrid);
for (IssueMetricFormula formula : formulaFactory.getFormulas()) {
// exclude leak formulas when leak period is not defined
if (beginningOfLeakPeriod.isPresent() || !formula.isOnLeak()) {
context.change(c, formula);
try {
formula.compute(context, issueCounter);
} catch (RuntimeException e) {
throw new IllegalStateException("Fail to compute " + formula.getMetric().getKey() + " on " + context.getComponent().getDbKey(), e);
}
}
}
});

EvaluatedQualityGate evaluatedQualityGate = qGateComputer.refreshGateStatus(project, qualityGate, matrix);

// persist the measures that have been created or updated
matrix.getChanged().forEach(m -> dbClient.liveMeasureDao().insertOrUpdate(dbSession, m, null));
projectIndexer.commitAndIndex(dbSession, singleton(project), ProjectIndexer.Cause.MEASURE_CHANGE);

return Optional.of(new QGChangeEvent(project, branch, lastAnalysis.get(), config, () -> Optional.of(evaluatedQualityGate)));
}

private List<ComponentDto> loadTreeOfComponents(DbSession dbSession, List<ComponentDto> touchedComponents) {
Set<String> componentUuids = new HashSet<>();
for (ComponentDto component : touchedComponents) {
componentUuids.add(component.uuid());
// ancestors, excluding self
componentUuids.addAll(component.getUuidPathAsList());
}
// Contrary to the formulas in Compute Engine,
// measures do not aggregate values of descendant components.
// As a consequence nodes do not need to be sorted. Formulas can be applied
// on components in any order.
return dbClient.componentDao().selectByUuids(dbSession, componentUuids);
}

private Set<String> getKeysOfAllInvolvedMetrics(QualityGate gate) {
Set<String> metricKeys = new HashSet<>();
for (Metric metric : formulaFactory.getFormulaMetrics()) {
metricKeys.add(metric.getKey());
}
metricKeys.addAll(qGateComputer.getMetricsRelatedTo(gate));
return metricKeys;
}

private static ComponentDto findProject(Collection<ComponentDto> components) {
return components.stream().filter(ComponentDto::isRootProject).findFirst()
.orElseThrow(() -> new IllegalStateException("No project found in " + components));
}

private BranchDto loadBranch(DbSession dbSession, ComponentDto project) {
return dbClient.branchDao().selectByUuid(dbSession, project.uuid())
.orElseThrow(() -> new IllegalStateException("Branch not found: " + project.uuid()));
}

private OrganizationDto loadOrganization(DbSession dbSession, ComponentDto project) {
String organizationUuid = project.getOrganizationUuid();
return dbClient.organizationDao().selectByUuid(dbSession, organizationUuid)
.orElseThrow(() -> new IllegalStateException("No organization with UUID " + organizationUuid));
}

private static class FormulaContextImpl implements IssueMetricFormula.Context {
private final MeasureMatrix matrix;
private final DebtRatingGrid debtRatingGrid;
private ComponentDto currentComponent;
private IssueMetricFormula currentFormula;

private FormulaContextImpl(MeasureMatrix matrix, DebtRatingGrid debtRatingGrid) {
this.matrix = matrix;
this.debtRatingGrid = debtRatingGrid;
}

private void change(ComponentDto component, IssueMetricFormula formula) {
this.currentComponent = component;
this.currentFormula = formula;
}

@Override
public ComponentDto getComponent() {
return currentComponent;
}

@Override
public DebtRatingGrid getDebtRatingGrid() {
return debtRatingGrid;
}

@Override
public Optional<Double> getValue(Metric metric) {
Optional<LiveMeasureDto> measure = matrix.getMeasure(currentComponent, metric.getKey());
return measure.map(LiveMeasureDto::getValue);
}

@Override
public Optional<Double> getLeakValue(Metric metric) {
Optional<LiveMeasureDto> measure = matrix.getMeasure(currentComponent, metric.getKey());
return measure.map(LiveMeasureDto::getVariation);
}

@Override
public void setValue(double value) {
String metricKey = currentFormula.getMetric().getKey();
checkState(!currentFormula.isOnLeak(), "Formula of metric %s accepts only leak values", metricKey);
matrix.setValue(currentComponent, metricKey, value);
}

@Override
public void setLeakValue(double value) {
String metricKey = currentFormula.getMetric().getKey();
checkState(currentFormula.isOnLeak(), "Formula of metric %s does not accept leak values", metricKey);
matrix.setLeakValue(currentComponent, metricKey, value);
}

@Override
public void setValue(Rating value) {
String metricKey = currentFormula.getMetric().getKey();
checkState(!currentFormula.isOnLeak(), "Formula of metric %s accepts only leak values", metricKey);
matrix.setValue(currentComponent, metricKey, value);
}

@Override
public void setLeakValue(Rating value) {
String metricKey = currentFormula.getMetric().getKey();
checkState(currentFormula.isOnLeak(), "Formula of metric %s does not accept leak values", metricKey);
matrix.setLeakValue(currentComponent, metricKey, value);
}
}
}

+ 32
- 0
server/sonar-server/src/main/java/org/sonar/server/measure/live/LiveMeasureModule.java Visa fil

@@ -0,0 +1,32 @@
/*
* 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.measure.live;

import org.sonar.core.platform.Module;

public class LiveMeasureModule extends Module {
@Override
protected void configureModule() {
add(
IssueMetricFormulaFactoryImpl.class,
LiveMeasureComputerImpl.class,
LiveQualityGateComputerImpl.class);
}
}

+ 40
- 0
server/sonar-server/src/main/java/org/sonar/server/measure/live/LiveQualityGateComputer.java Visa fil

@@ -0,0 +1,40 @@
/*
* 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.measure.live;

import java.util.Set;
import org.sonar.api.server.ServerSide;
import org.sonar.db.DbSession;
import org.sonar.db.component.BranchDto;
import org.sonar.db.component.ComponentDto;
import org.sonar.db.organization.OrganizationDto;
import org.sonar.server.qualitygate.EvaluatedQualityGate;
import org.sonar.server.qualitygate.QualityGate;

@ServerSide
public interface LiveQualityGateComputer {

QualityGate loadQualityGate(DbSession dbSession, OrganizationDto organization, ComponentDto project, BranchDto branch);

EvaluatedQualityGate refreshGateStatus(ComponentDto project, QualityGate gate, MeasureMatrix measureMatrix);

Set<String> getMetricsRelatedTo(QualityGate gate);

}

+ 151
- 0
server/sonar-server/src/main/java/org/sonar/server/measure/live/LiveQualityGateComputerImpl.java Visa fil

@@ -0,0 +1,151 @@
/*
* 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.measure.live;

import java.util.Collection;
import java.util.HashSet;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import java.util.OptionalDouble;
import java.util.Set;
import org.sonar.api.measures.CoreMetrics;
import org.sonar.api.measures.Metric;
import org.sonar.db.DbClient;
import org.sonar.db.DbSession;
import org.sonar.db.component.BranchDto;
import org.sonar.db.component.BranchType;
import org.sonar.db.component.ComponentDto;
import org.sonar.db.measure.LiveMeasureDto;
import org.sonar.db.metric.MetricDto;
import org.sonar.db.organization.OrganizationDto;
import org.sonar.db.qualitygate.QualityGateConditionDto;
import org.sonar.db.qualitygate.QualityGateDto;
import org.sonar.server.qualitygate.Condition;
import org.sonar.server.qualitygate.EvaluatedQualityGate;
import org.sonar.server.qualitygate.QualityGate;
import org.sonar.server.qualitygate.QualityGateConverter;
import org.sonar.server.qualitygate.QualityGateEvaluator;
import org.sonar.server.qualitygate.QualityGateFinder;
import org.sonar.server.qualitygate.ShortLivingBranchQualityGate;

import static org.sonar.core.util.stream.MoreCollectors.toHashSet;
import static org.sonar.core.util.stream.MoreCollectors.uniqueIndex;

public class LiveQualityGateComputerImpl implements LiveQualityGateComputer {

private final DbClient dbClient;
private final QualityGateFinder qGateFinder;
private final QualityGateEvaluator evaluator;

public LiveQualityGateComputerImpl(DbClient dbClient, QualityGateFinder qGateFinder, QualityGateEvaluator evaluator) {
this.dbClient = dbClient;
this.qGateFinder = qGateFinder;
this.evaluator = evaluator;
}

@Override
public QualityGate loadQualityGate(DbSession dbSession, OrganizationDto organization, ComponentDto project, BranchDto branch) {
if (branch.getBranchType() == BranchType.SHORT) {
return ShortLivingBranchQualityGate.GATE;
}

ComponentDto mainProject = project.getMainBranchProjectUuid() == null ? project : dbClient.componentDao().selectOrFailByKey(dbSession, project.getKey());
QualityGateDto gateDto = qGateFinder.getQualityGate(dbSession, organization, mainProject).getQualityGate();
Collection<QualityGateConditionDto> conditionDtos = dbClient.gateConditionDao().selectForQualityGate(dbSession, gateDto.getId());
Set<Integer> metricIds = conditionDtos.stream().map(c -> (int) c.getMetricId())
.collect(toHashSet(conditionDtos.size()));
Map<Integer, MetricDto> metricsById = dbClient.metricDao().selectByIds(dbSession, metricIds).stream()
.collect(uniqueIndex(MetricDto::getId));

Set<Condition> conditions = conditionDtos.stream().map(conditionDto -> {
String metricKey = metricsById.get((int) conditionDto.getMetricId()).getKey();
Condition.Operator operator = Condition.Operator.fromDbValue(conditionDto.getOperator());
boolean onLeak = Objects.equals(conditionDto.getPeriod(), 1);
return new Condition(metricKey, operator, conditionDto.getErrorThreshold(), conditionDto.getWarningThreshold(), onLeak);
}).collect(toHashSet(conditionDtos.size()));

return new QualityGate(String.valueOf(gateDto.getId()), gateDto.getName(), conditions);
}

@Override
public EvaluatedQualityGate refreshGateStatus(ComponentDto project, QualityGate gate, MeasureMatrix measureMatrix) {
QualityGateEvaluator.Measures measures = metricKey -> {
Optional<LiveMeasureDto> liveMeasureDto = measureMatrix.getMeasure(project, metricKey);
if (!liveMeasureDto.isPresent()) {
return Optional.empty();
}
MetricDto metric = measureMatrix.getMetric(liveMeasureDto.get().getMetricId());
return Optional.of(new LiveMeasure(liveMeasureDto.get(), metric));
};

EvaluatedQualityGate evaluatedGate = evaluator.evaluate(gate, measures);

measureMatrix.setValue(project, CoreMetrics.ALERT_STATUS_KEY, evaluatedGate.getStatus().name());
measureMatrix.setValue(project, CoreMetrics.QUALITY_GATE_DETAILS_KEY, QualityGateConverter.toJson(evaluatedGate));

return evaluatedGate;
}

@Override
public Set<String> getMetricsRelatedTo(QualityGate gate) {
Set<String> metricKeys = new HashSet<>();
metricKeys.add(CoreMetrics.ALERT_STATUS_KEY);
metricKeys.add(CoreMetrics.QUALITY_GATE_DETAILS_KEY);
metricKeys.addAll(evaluator.getMetricKeys(gate));
return metricKeys;
}

private static class LiveMeasure implements QualityGateEvaluator.Measure {
private final LiveMeasureDto dto;
private final MetricDto metric;

LiveMeasure(LiveMeasureDto dto, MetricDto metric) {
this.dto = dto;
this.metric = metric;
}

@Override
public Metric.ValueType getType() {
return Metric.ValueType.valueOf(metric.getValueType());
}

@Override
public OptionalDouble getValue() {
if (dto.getValue() == null) {
return OptionalDouble.empty();
}
return OptionalDouble.of(dto.getValue());
}

@Override
public Optional<String> getStringValue() {
return Optional.ofNullable(dto.getTextValue());
}

@Override
public OptionalDouble getLeakValue() {
if (dto.getVariation() == null) {
return OptionalDouble.empty();
}
return OptionalDouble.of(dto.getVariation());
}
}
}

+ 203
- 0
server/sonar-server/src/main/java/org/sonar/server/measure/live/MeasureMatrix.java Visa fil

@@ -0,0 +1,203 @@
/*
* 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.measure.live;

import com.google.common.collect.ArrayTable;
import com.google.common.collect.Collections2;
import com.google.common.collect.Table;
import java.math.BigDecimal;
import java.math.RoundingMode;
import java.util.Collection;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import java.util.function.Function;
import java.util.stream.Stream;
import javax.annotation.Nullable;
import org.sonar.db.component.ComponentDto;
import org.sonar.db.measure.LiveMeasureDto;
import org.sonar.db.metric.MetricDto;
import org.sonar.server.computation.task.projectanalysis.qualitymodel.Rating;

import static com.google.common.base.Preconditions.checkArgument;
import static java.util.Objects.requireNonNull;

/**
* Keep the measures in memory during refresh of live measures:
* <ul>
* <li>the values of last analysis, restricted to the needed metrics</li>
* <li>the refreshed values</li>
* </ul>
*/
class MeasureMatrix {

// component uuid -> metric key -> measure
private final Table<String, String, MeasureCell> table;

private final Map<String, MetricDto> metricsByKeys = new HashMap<>();
private final Map<Integer, MetricDto> metricsByIds = new HashMap<>();

MeasureMatrix(Collection<ComponentDto> components, Collection<MetricDto> metrics, List<LiveMeasureDto> dbMeasures) {
for (MetricDto metric : metrics) {
this.metricsByKeys.put(metric.getKey(), metric);
this.metricsByIds.put(metric.getId(), metric);
}
this.table = ArrayTable.create(Collections2.transform(components, ComponentDto::uuid), metricsByKeys.keySet());
for (LiveMeasureDto dbMeasure : dbMeasures) {
table.put(dbMeasure.getComponentUuid(), metricsByIds.get(dbMeasure.getMetricId()).getKey(), new MeasureCell(dbMeasure, false));
}
}

MetricDto getMetric(int id) {
return requireNonNull(metricsByIds.get(id), () -> String.format("Metric with id %d not found", id));
}

private MetricDto getMetric(String key) {
return requireNonNull(metricsByKeys.get(key), () -> String.format("Metric with key %s not found", key));
}

Optional<LiveMeasureDto> getMeasure(ComponentDto component, String metricKey) {
checkArgument(table.containsColumn(metricKey), "Metric with key %s is not registered", metricKey);
MeasureCell cell = table.get(component.uuid(), metricKey);
return cell == null ? Optional.empty() : Optional.of(cell.measure);
}

void setValue(ComponentDto component, String metricKey, double value) {
changeCell(component, metricKey, m -> {
MetricDto metric = getMetric(metricKey);
double newValue = scale(metric, value);

Double initialValue = m.getValue();
if (initialValue != null && Double.compare(initialValue, newValue) == 0) {
return false;
}
m.setValue(newValue);
Double initialVariation = m.getVariation();
if (initialValue != null && initialVariation != null) {
double leakInitialValue = initialValue - initialVariation;
m.setVariation(scale(metric, value - leakInitialValue));
}
return true;
});
}

void setValue(ComponentDto component, String metricKey, Rating value) {
changeCell(component, metricKey, m -> {
Double initialValue = m.getValue();
if (initialValue != null && Double.compare(initialValue, (double) value.getIndex()) == 0) {
return false;
}
m.setData(value.name());
m.setValue((double) value.getIndex());

Double initialVariation = m.getVariation();
if (initialValue != null && initialVariation != null) {
double leakInitialValue = initialValue - initialVariation;
m.setVariation(value.getIndex() - leakInitialValue);
}
return true;
});
}

void setValue(ComponentDto component, String metricKey, @Nullable String data) {
changeCell(component, metricKey, m -> {
if (Objects.equals(m.getDataAsString(), data)) {
return false;
}
m.setData(data);
return true;
});
}

void setLeakValue(ComponentDto component, String metricKey, double variation) {
changeCell(component, metricKey, c -> {
double newVariation = scale(getMetric(metricKey), variation);
if (c.getVariation() != null && Double.compare(c.getVariation(), newVariation) == 0) {
return false;
}
MetricDto metric = metricsByKeys.get(metricKey);
c.setVariation(scale(metric, variation));
return true;
});
}

void setLeakValue(ComponentDto component, String metricKey, Rating variation) {
setLeakValue(component, metricKey, (double) variation.getIndex());
}

Stream<LiveMeasureDto> getChanged() {
return table.values()
.stream()
.filter(Objects::nonNull)
.filter(MeasureCell::isChanged)
.map(MeasureCell::getMeasure);
}

private void changeCell(ComponentDto component, String metricKey, Function<LiveMeasureDto, Boolean> changer) {
MeasureCell cell = table.get(component.uuid(), metricKey);
if (cell == null) {
LiveMeasureDto measure = new LiveMeasureDto()
.setComponentUuid(component.uuid())
.setProjectUuid(component.projectUuid())
.setMetricId(metricsByKeys.get(metricKey).getId());
cell = new MeasureCell(measure, true);
table.put(component.uuid(), metricKey, cell);
changer.apply(cell.getMeasure());
} else if (changer.apply(cell.getMeasure())) {
cell.setChanged(true);
}
}

/**
* Round a measure value by applying the scale defined on the metric.
* Example: scale(0.1234) returns 0.12 if metric scale is 2
*/
private static double scale(MetricDto metric, double value) {
if (metric.getDecimalScale() == null) {
return value;
}
BigDecimal bd = BigDecimal.valueOf(value);
return bd.setScale(metric.getDecimalScale(), RoundingMode.HALF_UP).doubleValue();
}

private static class MeasureCell {
private final LiveMeasureDto measure;
private boolean changed;

private MeasureCell(LiveMeasureDto measure, boolean changed) {
this.measure = measure;
this.changed = changed;
}

public LiveMeasureDto getMeasure() {
return measure;
}

public boolean isChanged() {
return changed;
}

public void setChanged(boolean b) {
this.changed = b;
}
}
}

+ 23
- 0
server/sonar-server/src/main/java/org/sonar/server/measure/live/package-info.java Visa fil

@@ -0,0 +1,23 @@
/*
* 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.
*/
@ParametersAreNonnullByDefault
package org.sonar.server.measure.live;

import javax.annotation.ParametersAreNonnullByDefault;

+ 1
- 1
server/sonar-server/src/main/java/org/sonar/server/measure/ws/ComponentAction.java Visa fil

@@ -216,7 +216,7 @@ public class ComponentAction implements MeasuresWsAction {

private List<LiveMeasureDto> searchMeasures(DbSession dbSession, ComponentDto component, List<MetricDto> metrics) {
List<Integer> metricIds = Lists.transform(metrics, MetricDto::getId);
List<LiveMeasureDto> measures = dbClient.liveMeasureDao().selectByComponentUuids(dbSession, singletonList(component.uuid()), metricIds);
List<LiveMeasureDto> measures = dbClient.liveMeasureDao().selectByComponentUuidsAndMetricIds(dbSession, singletonList(component.uuid()), metricIds);
addBestValuesToMeasures(measures, component, metrics);
return measures;
}

+ 1
- 1
server/sonar-server/src/main/java/org/sonar/server/measure/ws/SearchAction.java Visa fil

@@ -162,7 +162,7 @@ public class SearchAction implements MeasuresWsAction {
}

private List<LiveMeasureDto> searchMeasures() {
return dbClient.liveMeasureDao().selectByComponentUuids(dbSession,
return dbClient.liveMeasureDao().selectByComponentUuidsAndMetricIds(dbSession,
projects.stream().map(ComponentDto::uuid).collect(MoreCollectors.toArrayList(projects.size())),
metrics.stream().map(MetricDto::getId).collect(MoreCollectors.toArrayList(metrics.size())));
}

+ 2
- 2
server/sonar-server/src/main/java/org/sonar/server/permission/index/PermissionIndexer.java Visa fil

@@ -45,7 +45,6 @@ import org.sonar.server.permission.index.PermissionIndexerDao.Dto;
import static java.util.Collections.emptyList;
import static org.sonar.core.util.stream.MoreCollectors.toArrayList;
import static org.sonar.core.util.stream.MoreCollectors.toSet;
import static org.sonar.server.es.DefaultIndexSettings.REFRESH_IMMEDIATE;

/**
* Populates the types "authorization" of each index requiring project
@@ -100,9 +99,10 @@ public class PermissionIndexer implements ProjectIndexer {
@Override
public Collection<EsQueueDto> prepareForRecovery(DbSession dbSession, Collection<String> projectUuids, ProjectIndexer.Cause cause) {
switch (cause) {
case MEASURE_CHANGE:
case PROJECT_KEY_UPDATE:
case PROJECT_TAGS_UPDATE:
// nothing to change, project key and tags are not part of this index
// nothing to change. Measures, project key and tags are not part of this index
return emptyList();

case PROJECT_CREATION:

+ 4
- 0
server/sonar-server/src/main/java/org/sonar/server/platform/platformlevel/PlatformLevel4.java Visa fil

@@ -69,6 +69,7 @@ import org.sonar.server.health.NodeHealthModule;
import org.sonar.server.issue.AddTagsAction;
import org.sonar.server.issue.AssignAction;
import org.sonar.server.issue.CommentAction;
import org.sonar.server.issue.IssueChangePostProcessorImpl;
import org.sonar.server.issue.RemoveTagsAction;
import org.sonar.server.issue.SetSeverityAction;
import org.sonar.server.issue.SetTypeAction;
@@ -88,6 +89,7 @@ import org.sonar.server.issue.ws.IssueWsModule;
import org.sonar.server.language.ws.LanguageWs;
import org.sonar.server.measure.custom.ws.CustomMeasuresWsModule;
import org.sonar.server.measure.index.ProjectsEsModule;
import org.sonar.server.measure.live.LiveMeasureModule;
import org.sonar.server.measure.ws.MeasuresWsModule;
import org.sonar.server.measure.ws.TimeMachineWs;
import org.sonar.server.metric.CoreCustomMetrics;
@@ -409,6 +411,7 @@ public class PlatformLevel4 extends PlatformLevel {
ComponentIndexDefinition.class,
ComponentIndex.class,
ComponentIndexer.class,
LiveMeasureModule.class,

FavoriteModule.class,

@@ -444,6 +447,7 @@ public class PlatformLevel4 extends PlatformLevel {
TransitionAction.class,
AddTagsAction.class,
RemoveTagsAction.class,
IssueChangePostProcessorImpl.class,

// technical debt
DebtModelPluginRepository.class,

+ 16
- 3
server/sonar-server/src/main/java/org/sonar/server/qualitygate/Condition.java Visa fil

@@ -21,10 +21,13 @@ package org.sonar.server.qualitygate;

import java.util.Objects;
import java.util.Optional;
import java.util.stream.Stream;
import javax.annotation.CheckForNull;
import javax.annotation.Nullable;
import javax.annotation.concurrent.Immutable;
import org.sonar.db.qualitygate.QualityGateConditionDto;

import static com.google.common.base.Strings.emptyToNull;
import static java.util.Objects.requireNonNull;

@Immutable
@@ -44,8 +47,8 @@ public class Condition {
this.metricKey = requireNonNull(metricKey, "metricKey can't be null");
this.operator = requireNonNull(operator, "operator can't be null");
this.onLeakPeriod = onLeakPeriod;
this.errorThreshold = errorThreshold;
this.warningThreshold = warningThreshold;
this.errorThreshold = emptyToNull(errorThreshold);
this.warningThreshold = emptyToNull(warningThreshold);
}

public String getMetricKey() {
@@ -108,7 +111,10 @@ public class Condition {
}

public enum Operator {
EQUALS("EQ"), NOT_EQUALS("NE"), GREATER_THAN("GT"), LESS_THAN("LT");
EQUALS(QualityGateConditionDto.OPERATOR_EQUALS),
NOT_EQUALS(QualityGateConditionDto.OPERATOR_NOT_EQUALS),
GREATER_THAN(QualityGateConditionDto.OPERATOR_GREATER_THAN),
LESS_THAN(QualityGateConditionDto.OPERATOR_LESS_THAN);

private final String dbValue;

@@ -119,5 +125,12 @@ public class Condition {
public String getDbValue() {
return dbValue;
}

public static Operator fromDbValue(String s) {
return Stream.of(values())
.filter(o -> o.getDbValue().equals(s))
.findFirst()
.orElseThrow(() -> new IllegalArgumentException("Unsupported operator db value: " + s));
}
}
}

+ 181
- 0
server/sonar-server/src/main/java/org/sonar/server/qualitygate/ConditionEvaluator.java Visa fil

@@ -0,0 +1,181 @@
/*
* 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.qualitygate;

import java.util.EnumSet;
import java.util.Optional;
import java.util.Set;
import javax.annotation.CheckForNull;
import org.sonar.api.measures.Metric.ValueType;
import org.sonar.server.qualitygate.EvaluatedCondition.EvaluationStatus;

import static java.util.Optional.of;
import static org.sonar.api.measures.Metric.ValueType.BOOL;
import static org.sonar.api.measures.Metric.ValueType.FLOAT;
import static org.sonar.api.measures.Metric.ValueType.INT;
import static org.sonar.api.measures.Metric.ValueType.MILLISEC;
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;

class ConditionEvaluator {

private static final Set<ValueType> NUMERICAL_TYPES = EnumSet.of(BOOL, INT, RATING, FLOAT, MILLISEC, PERCENT, WORK_DUR);

private ConditionEvaluator() {
// prevent instantiation
}

/**
* Evaluates the condition for the specified measure
*/
static EvaluatedCondition evaluate(Condition condition, QualityGateEvaluator.Measures measures) {
Optional<QualityGateEvaluator.Measure> measure = measures.get(condition.getMetricKey());
if (!measure.isPresent()) {
return new EvaluatedCondition(condition, EvaluationStatus.OK, null);
}

Optional<Comparable> value = getMeasureValue(condition, measure.get());
if (!value.isPresent()) {
return new EvaluatedCondition(condition, EvaluationStatus.OK, null);
}

ValueType type = measure.get().getType();
return evaluateCondition(condition, type, value.get(), true)
.orElseGet(() -> evaluateCondition(condition, type, value.get(), false)
.orElseGet(() -> new EvaluatedCondition(condition, EvaluationStatus.OK, value.get().toString())));
}

/**
* Evaluates the error or warning condition. Returns empty if threshold or measure value is not defined.
*/
private static Optional<EvaluatedCondition> evaluateCondition(Condition condition, ValueType type, Comparable value, boolean error) {
Optional<Comparable> threshold = getThreshold(condition, type, error);
if (!threshold.isPresent()) {
return Optional.empty();
}

if (reachThreshold(value, threshold.get(), condition)) {
EvaluationStatus status = error ? EvaluationStatus.ERROR : EvaluationStatus.WARN;
return of(new EvaluatedCondition(condition, status, value.toString()));
}
return Optional.empty();
}

private static Optional<Comparable> getThreshold(Condition condition, ValueType valueType, boolean error) {
Optional<String> valString = error ? condition.getErrorThreshold() : condition.getWarningThreshold();
return valString.map(s -> {
try {
switch (valueType) {
case BOOL:
return parseInteger(s) == 1;
case INT:
case RATING:
return parseInteger(s);
case MILLISEC:
case WORK_DUR:
return Long.parseLong(s);
case FLOAT:
case PERCENT:
return Double.parseDouble(s);
case STRING:
case LEVEL:
return s;
default:
throw new IllegalArgumentException(String.format("Unsupported value type %s. Cannot convert condition value", valueType));
}
} catch (NumberFormatException badValueFormat) {
throw new IllegalArgumentException(String.format(
"Quality Gate: unable to parse threshold '%s' to compare against %s", s, condition.getMetricKey()));
}
});
}

private static Optional<Comparable> getMeasureValue(Condition condition, QualityGateEvaluator.Measure measure) {
if (condition.isOnLeakPeriod()) {
return Optional.ofNullable(getLeakValue(measure));
}

return Optional.ofNullable(getValue(measure));
}

@CheckForNull
private static Comparable getValue(QualityGateEvaluator.Measure measure) {
if (NUMERICAL_TYPES.contains(measure.getType())) {
return measure.getValue().isPresent() ? getNumericValue(measure.getType(), measure.getValue().getAsDouble()) : null;
}

switch (measure.getType()) {
case LEVEL:
case STRING:
case DISTRIB:
return measure.getStringValue().orElse(null);
default:
throw new IllegalArgumentException("Condition on leak period is not allowed for type " + measure.getType());
}
}

@CheckForNull
private static Comparable getLeakValue(QualityGateEvaluator.Measure measure) {
if (NUMERICAL_TYPES.contains(measure.getType())) {
return measure.getLeakValue().isPresent() ? getNumericValue(measure.getType(), measure.getLeakValue().getAsDouble()) : null;
}

throw new IllegalArgumentException("Condition on leak period is not allowed for type " + measure.getType());
}

private static Comparable getNumericValue(ValueType type, double value) {
switch (type) {
case BOOL:
return Double.compare(value, 1.0) == 1;
case INT:
case RATING:
return (int) value;
case FLOAT:
case PERCENT:
return value;
case MILLISEC:
case WORK_DUR:
return (long) value;
default:
throw new IllegalArgumentException("Condition on numeric value is not allowed for type " + type);
}
}

private static int parseInteger(String value) {
return value.contains(".") ? Integer.parseInt(value.substring(0, value.indexOf('.'))) : Integer.parseInt(value);
}

private static boolean reachThreshold(Comparable measureValue, Comparable threshold, Condition condition) {
int comparison = measureValue.compareTo(threshold);
switch (condition.getOperator()) {
case EQUALS:
return comparison == 0;
case NOT_EQUALS:
return comparison != 0;
case GREATER_THAN:
return comparison > 0;
case LESS_THAN:
return comparison < 0;
default:
throw new IllegalArgumentException(String.format("Unsupported operator '%s'", condition.getOperator()));
}
}
}

+ 2
- 1
server/sonar-server/src/main/java/org/sonar/server/qualitygate/EvaluatedCondition.java Visa fil

@@ -30,6 +30,7 @@ import static java.util.Objects.requireNonNull;
public class EvaluatedCondition {
private final Condition condition;
private final EvaluationStatus status;
@Nullable
private final String value;

public EvaluatedCondition(Condition condition, EvaluationStatus status, @Nullable String value) {
@@ -74,7 +75,7 @@ public class EvaluatedCondition {
return "EvaluatedCondition{" +
"condition=" + condition +
", status=" + status +
", value=" + (value == null ? null : '\'' + value + '\'') +
", value=" + (value == null ? null : ('\'' + value + '\'')) +
'}';
}


+ 29
- 27
server/sonar-server/src/main/java/org/sonar/server/qualitygate/EvaluatedQualityGate.java Visa fil

@@ -27,6 +27,7 @@ import java.util.Set;
import java.util.stream.Collectors;
import javax.annotation.Nullable;
import javax.annotation.concurrent.Immutable;
import org.sonar.api.measures.Metric;
import org.sonar.server.qualitygate.EvaluatedCondition.EvaluationStatus;

import static com.google.common.base.Preconditions.checkArgument;
@@ -35,20 +36,22 @@ import static java.util.Objects.requireNonNull;
@Immutable
public class EvaluatedQualityGate {
private final QualityGate qualityGate;
private final Status status;
private final Metric.Level status;
private final Set<EvaluatedCondition> evaluatedConditions;
private final boolean ignoredConditionsOnSmallChangeset;

private EvaluatedQualityGate(QualityGate qualityGate, Status status, Set<EvaluatedCondition> evaluatedConditions) {
this.qualityGate = qualityGate;
this.status = status;
private EvaluatedQualityGate(QualityGate qualityGate, Metric.Level status, Set<EvaluatedCondition> evaluatedConditions, boolean ignoredConditionsOnSmallChangeset) {
this.qualityGate = requireNonNull(qualityGate, "qualityGate can't be null");
this.status = requireNonNull(status, "status can't be null");
this.evaluatedConditions = evaluatedConditions;
this.ignoredConditionsOnSmallChangeset = ignoredConditionsOnSmallChangeset;
}

public QualityGate getQualityGate() {
return qualityGate;
}

public Status getStatus() {
public Metric.Level getStatus() {
return status;
}

@@ -56,6 +59,10 @@ public class EvaluatedQualityGate {
return evaluatedConditions;
}

public boolean hasIgnoredConditionsOnSmallChangeset() {
return ignoredConditionsOnSmallChangeset;
}

public static Builder newBuilder() {
return new Builder();
}
@@ -90,20 +97,26 @@ public class EvaluatedQualityGate {

public static final class Builder {
private QualityGate qualityGate;
private Status status;
private Metric.Level status;
private final Map<Condition, EvaluatedCondition> evaluatedConditions = new HashMap<>();
private boolean ignoredConditionsOnSmallChangeset = false;

private Builder() {
// use static factory method
}

public Builder setQualityGate(QualityGate qualityGate) {
this.qualityGate = checkQualityGate(qualityGate);
this.qualityGate = qualityGate;
return this;
}

public Builder setStatus(Metric.Level status) {
this.status = status;
return this;
}

public Builder setStatus(Status status) {
this.status = checkStatus(status);
public Builder setIgnoredConditionsOnSmallChangeset(boolean b) {
this.ignoredConditionsOnSmallChangeset = b;
return this;
}

@@ -112,18 +125,21 @@ public class EvaluatedQualityGate {
return this;
}

public Builder addCondition(EvaluatedCondition c) {
evaluatedConditions.put(c.getCondition(), c);
return this;
}

public Set<EvaluatedCondition> getEvaluatedConditions() {
return ImmutableSet.copyOf(evaluatedConditions.values());
}

public EvaluatedQualityGate build() {
checkQualityGate(this.qualityGate);
checkStatus(this.status);

return new EvaluatedQualityGate(
this.qualityGate,
this.status,
checkEvaluatedConditions(qualityGate, evaluatedConditions));
checkEvaluatedConditions(qualityGate, evaluatedConditions),
ignoredConditionsOnSmallChangeset);
}

private static Set<EvaluatedCondition> checkEvaluatedConditions(QualityGate qualityGate, Map<Condition, EvaluatedCondition> evaluatedConditions) {
@@ -141,19 +157,5 @@ public class EvaluatedQualityGate {

return ImmutableSet.copyOf(evaluatedConditions.values());
}

private static QualityGate checkQualityGate(@Nullable QualityGate qualityGate) {
return requireNonNull(qualityGate, "qualityGate can't be null");
}

private static Status checkStatus(@Nullable Status status) {
return requireNonNull(status, "status can't be null");
}
}

public enum Status {
OK,
WARN,
ERROR
}
}

+ 0
- 134
server/sonar-server/src/main/java/org/sonar/server/qualitygate/LiveQualityGateFactoryImpl.java Visa fil

@@ -1,134 +0,0 @@
/*
* 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.qualitygate;

import java.util.LinkedHashMap;
import java.util.Map;
import java.util.Set;
import org.elasticsearch.action.search.SearchResponse;
import org.sonar.api.measures.CoreMetrics;
import org.sonar.api.rules.RuleType;
import org.sonar.api.utils.System2;
import org.sonar.db.component.ComponentDto;
import org.sonar.db.qualitygate.QualityGateConditionDto;
import org.sonar.server.es.Facets;
import org.sonar.server.es.SearchOptions;
import org.sonar.server.issue.IssueQuery;
import org.sonar.server.issue.index.IssueIndex;
import org.sonar.server.rule.index.RuleIndex;

import static java.lang.String.format;
import static java.lang.String.valueOf;
import static java.util.Collections.singletonList;
import static org.sonar.core.util.stream.MoreCollectors.toSet;

public class LiveQualityGateFactoryImpl implements LiveQualityGateFactory {

private final IssueIndex issueIndex;
private final System2 system2;

public LiveQualityGateFactoryImpl(IssueIndex issueIndex, System2 system2) {
this.issueIndex = issueIndex;
this.system2 = system2;
}

@Override
public EvaluatedQualityGate buildForShortLivedBranch(ComponentDto componentDto) {
return createQualityGate(componentDto, issueIndex);
}

private EvaluatedQualityGate createQualityGate(ComponentDto project, IssueIndex issueIndex) {
SearchResponse searchResponse = issueIndex.search(IssueQuery.builder()
.projectUuids(singletonList(project.getMainBranchProjectUuid()))
.branchUuid(project.uuid())
.mainBranch(false)
.resolved(false)
.checkAuthorization(false)
.build(),
new SearchOptions().addFacets(RuleIndex.FACET_TYPES));
LinkedHashMap<String, Long> typeFacet = new Facets(searchResponse, system2.getDefaultTimeZone())
.get(RuleIndex.FACET_TYPES);

EvaluatedQualityGate.Builder builder = EvaluatedQualityGate.newBuilder();
Set<Condition> conditions = ShortLivingBranchQualityGate.CONDITIONS.stream()
.map(c -> {
long measure = getMeasure(typeFacet, c);
EvaluatedCondition.EvaluationStatus status = measure > 0 ? EvaluatedCondition.EvaluationStatus.ERROR : EvaluatedCondition.EvaluationStatus.OK;
Condition condition = new Condition(c.getMetricKey(), toOperator(c), c.getErrorThreshold(), c.getWarnThreshold(), c.isOnLeak());
builder.addCondition(condition, status, valueOf(measure));
return condition;
})
.collect(toSet(ShortLivingBranchQualityGate.CONDITIONS.size()));
builder
.setQualityGate(
new org.sonar.server.qualitygate.QualityGate(
valueOf(ShortLivingBranchQualityGate.ID),
ShortLivingBranchQualityGate.NAME,
conditions))
.setStatus(qgStatusFrom(builder.getEvaluatedConditions()));

return builder.build();
}

private static Condition.Operator toOperator(ShortLivingBranchQualityGate.Condition c) {
String operator = c.getOperator();
switch (operator) {
case QualityGateConditionDto.OPERATOR_GREATER_THAN:
return Condition.Operator.GREATER_THAN;
case QualityGateConditionDto.OPERATOR_LESS_THAN:
return Condition.Operator.LESS_THAN;
case QualityGateConditionDto.OPERATOR_EQUALS:
return Condition.Operator.EQUALS;
case QualityGateConditionDto.OPERATOR_NOT_EQUALS:
return Condition.Operator.NOT_EQUALS;
default:
throw new IllegalArgumentException(format("Unsupported Condition operator '%s'", operator));
}
}

private static EvaluatedQualityGate.Status qgStatusFrom(Set<EvaluatedCondition> conditions) {
if (conditions.stream().anyMatch(c -> c.getStatus() == EvaluatedCondition.EvaluationStatus.ERROR)) {
return EvaluatedQualityGate.Status.ERROR;
}
return EvaluatedQualityGate.Status.OK;
}

private static long getMeasure(LinkedHashMap<String, Long> typeFacet, ShortLivingBranchQualityGate.Condition c) {
String metricKey = c.getMetricKey();
switch (metricKey) {
case CoreMetrics.BUGS_KEY:
return getValueForRuleType(typeFacet, RuleType.BUG);
case CoreMetrics.VULNERABILITIES_KEY:
return getValueForRuleType(typeFacet, RuleType.VULNERABILITY);
case CoreMetrics.CODE_SMELLS_KEY:
return getValueForRuleType(typeFacet, RuleType.CODE_SMELL);
default:
throw new IllegalArgumentException(format("Unsupported metric key '%s' in hardcoded quality gate", metricKey));
}
}

private static long getValueForRuleType(Map<String, Long> facet, RuleType ruleType) {
Long res = facet.get(ruleType.name());
if (res == null) {
return 0L;
}
return res;
}
}

+ 2
- 2
server/sonar-server/src/main/java/org/sonar/server/qualitygate/QualityGateConditionsUpdater.java Visa fil

@@ -34,7 +34,7 @@ import org.sonar.db.DbSession;
import org.sonar.db.metric.MetricDto;
import org.sonar.db.qualitygate.QualityGateConditionDto;
import org.sonar.db.qualitygate.QualityGateDto;
import org.sonar.server.computation.task.projectanalysis.qualitymodel.RatingGrid.Rating;
import org.sonar.server.computation.task.projectanalysis.qualitymodel.Rating;
import org.sonar.server.exceptions.NotFoundException;

import static com.google.common.base.Strings.isNullOrEmpty;
@@ -44,7 +44,7 @@ import static java.util.Arrays.stream;
import static org.sonar.api.measures.Metric.ValueType.RATING;
import static org.sonar.api.measures.Metric.ValueType.valueOf;
import static org.sonar.db.qualitygate.QualityGateConditionDto.isOperatorAllowed;
import static org.sonar.server.computation.task.projectanalysis.qualitymodel.RatingGrid.Rating.E;
import static org.sonar.server.computation.task.projectanalysis.qualitymodel.Rating.E;
import static org.sonar.server.qualitygate.ValidRatingMetrics.isCoreRatingMetric;
import static org.sonar.server.ws.WsUtils.checkRequest;


+ 61
- 0
server/sonar-server/src/main/java/org/sonar/server/qualitygate/QualityGateConverter.java Visa fil

@@ -0,0 +1,61 @@
/*
* 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.qualitygate;

import com.google.gson.JsonArray;
import com.google.gson.JsonObject;

public class QualityGateConverter {

private static final String FIELD_LEVEL = "level";
private static final String FIELD_IGNORED_CONDITIONS = "ignoredConditions";

private QualityGateConverter() {
// prevent instantiation
}

public static String toJson(EvaluatedQualityGate gate) {
JsonObject details = new JsonObject();
details.addProperty(FIELD_LEVEL, gate.getStatus().name());
JsonArray conditionResults = new JsonArray();
for (EvaluatedCondition condition : gate.getEvaluatedConditions()) {
conditionResults.add(toJson(condition));
}
details.add("conditions", conditionResults);
details.addProperty(FIELD_IGNORED_CONDITIONS, gate.hasIgnoredConditionsOnSmallChangeset());
return details.toString();
}

private static JsonObject toJson(EvaluatedCondition evaluatedCondition) {
Condition condition = evaluatedCondition.getCondition();

JsonObject result = new JsonObject();
result.addProperty("metric", condition.getMetricKey());
result.addProperty("op", condition.getOperator().getDbValue());
if (condition.isOnLeakPeriod()) {
result.addProperty("period", 1);
}
condition.getWarningThreshold().ifPresent(t -> result.addProperty("warning", t));
condition.getErrorThreshold().ifPresent(t -> result.addProperty("error", t));
evaluatedCondition.getValue().ifPresent(v -> result.addProperty("actual", v));
result.addProperty(FIELD_LEVEL, evaluatedCondition.getStatus().name());
return result;
}
}

+ 60
- 0
server/sonar-server/src/main/java/org/sonar/server/qualitygate/QualityGateEvaluator.java Visa fil

@@ -0,0 +1,60 @@
/*
* 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.qualitygate;

import java.util.Optional;
import java.util.OptionalDouble;
import java.util.Set;
import org.sonar.api.ce.ComputeEngineSide;
import org.sonar.api.measures.Metric;
import org.sonar.api.server.ServerSide;

@ComputeEngineSide
@ServerSide
public interface QualityGateEvaluator {

/**
* @param measures must provide the measures related to the metrics
* defined by {@link #getMetricKeys(QualityGate)}
*/
EvaluatedQualityGate evaluate(QualityGate gate, Measures measures);

/**
* Keys of the metrics involved in the computation of gate status.
* It may include metrics that are not part of conditions,
* for instance "new_lines" for the circuit-breaker on
* small changesets.
*/
Set<String> getMetricKeys(QualityGate gate);

interface Measures {
Optional<Measure> get(String metricKey);
}

interface Measure {
Metric.ValueType getType();

OptionalDouble getValue();

Optional<String> getStringValue();

OptionalDouble getLeakValue();
}
}

+ 133
- 0
server/sonar-server/src/main/java/org/sonar/server/qualitygate/QualityGateEvaluatorImpl.java Visa fil

@@ -0,0 +1,133 @@
/*
* 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.qualitygate;

import com.google.common.collect.ImmutableSet;
import com.google.common.collect.Multimap;
import java.util.Collection;
import java.util.HashSet;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
import java.util.function.Function;
import org.sonar.api.measures.CoreMetrics;
import org.sonar.api.measures.Metric.Level;
import org.sonar.core.util.stream.MoreCollectors;
import org.sonar.server.qualitygate.EvaluatedCondition.EvaluationStatus;

import static java.util.Objects.requireNonNull;
import static org.sonar.core.util.stream.MoreCollectors.toEnumSet;

public class QualityGateEvaluatorImpl implements QualityGateEvaluator {

private static final int MAXIMUM_NEW_LINES_FOR_SMALL_CHANGESETS = 20;
/**
* Some metrics will be ignored on very small change sets.
*/
private static final Set<String> METRICS_TO_IGNORE_ON_SMALL_CHANGESETS = ImmutableSet.of(
CoreMetrics.NEW_COVERAGE_KEY,
CoreMetrics.NEW_LINE_COVERAGE_KEY,
CoreMetrics.NEW_BRANCH_COVERAGE_KEY,
CoreMetrics.NEW_DUPLICATED_LINES_DENSITY_KEY,
CoreMetrics.NEW_DUPLICATED_LINES_KEY,
CoreMetrics.NEW_BLOCKS_DUPLICATED_KEY);

@Override
public EvaluatedQualityGate evaluate(QualityGate gate, Measures measures) {
EvaluatedQualityGate.Builder result = EvaluatedQualityGate.newBuilder()
.setQualityGate(gate);

boolean isSmallChangeset = isSmallChangeset(measures);
Multimap<String, Condition> conditionsPerMetric = gate.getConditions().stream()
.collect(MoreCollectors.index(Condition::getMetricKey, Function.identity()));

for (Map.Entry<String, Collection<Condition>> entry : conditionsPerMetric.asMap().entrySet()) {
String metricKey = entry.getKey();
Collection<Condition> conditionsOnSameMetric = entry.getValue();

EvaluatedCondition evaluation = evaluateConditionsOnMetric(conditionsOnSameMetric, measures);

if (isSmallChangeset && evaluation.getStatus() != EvaluationStatus.OK && METRICS_TO_IGNORE_ON_SMALL_CHANGESETS.contains(metricKey)) {
result.addCondition(new EvaluatedCondition(evaluation.getCondition(), EvaluationStatus.OK, evaluation.getValue().orElse(null)));
result.setIgnoredConditionsOnSmallChangeset(true);
} else {
result.addCondition(evaluation);
}
}

result.setStatus(overallStatusOf(result.getEvaluatedConditions()));

return result.build();
}

@Override
public Set<String> getMetricKeys(QualityGate gate) {
Set<String> metricKeys = new HashSet<>();
metricKeys.add(CoreMetrics.NEW_LINES_KEY);
for (Condition condition : gate.getConditions()) {
metricKeys.add(condition.getMetricKey());
}
return metricKeys;
}

private static boolean isSmallChangeset(Measures measures) {
Optional<Measure> newLines = measures.get(CoreMetrics.NEW_LINES_KEY);
return newLines.isPresent() &&
newLines.get().getLeakValue().isPresent() &&
newLines.get().getLeakValue().getAsDouble() < MAXIMUM_NEW_LINES_FOR_SMALL_CHANGESETS;
}

private static EvaluatedCondition evaluateConditionsOnMetric(Collection<Condition> conditionsOnSameMetric, Measures measures) {
EvaluatedCondition leakEvaluation = null;
EvaluatedCondition absoluteEvaluation = null;
for (Condition condition : conditionsOnSameMetric) {
if (condition.isOnLeakPeriod()) {
leakEvaluation = ConditionEvaluator.evaluate(condition, measures);
} else {
absoluteEvaluation = ConditionEvaluator.evaluate(condition, measures);
}
}

if (leakEvaluation == null) {
return requireNonNull(absoluteEvaluation, "Evaluation of absolute value can't be null on conditions " + conditionsOnSameMetric);
}
if (absoluteEvaluation == null) {
return requireNonNull(leakEvaluation, "Evaluation of leak value can't be null on conditions " + conditionsOnSameMetric);
}
// both conditions are present. Take the worse one. In case of equality, take
// the one on the leak period
if (absoluteEvaluation.getStatus().compareTo(leakEvaluation.getStatus()) > 0) {
return absoluteEvaluation;
}
return leakEvaluation;
}

private static Level overallStatusOf(Set<EvaluatedCondition> conditions) {
Set<EvaluationStatus> statuses = conditions.stream().map(EvaluatedCondition::getStatus).collect(toEnumSet(EvaluationStatus.class));
if (statuses.contains(EvaluationStatus.ERROR)) {
return Level.ERROR;
}
if (statuses.contains(EvaluationStatus.WARN)) {
return Level.WARN;
}
return Level.OK;
}

}

+ 6
- 6
server/sonar-server/src/main/java/org/sonar/server/qualitygate/QualityGateFinder.java Visa fil

@@ -22,6 +22,7 @@ package org.sonar.server.qualitygate;
import java.util.Optional;
import org.sonar.db.DbClient;
import org.sonar.db.DbSession;
import org.sonar.db.component.ComponentDto;
import org.sonar.db.organization.OrganizationDto;
import org.sonar.db.qualitygate.QGateWithOrgDto;
import org.sonar.db.qualitygate.QualityGateDto;
@@ -44,16 +45,15 @@ public class QualityGateFinder {
*
* It will first try to get the quality gate explicitly defined on a project, if none it will try to return default quality gate ofI the organization
*/
public QualityGateData getQualityGate(DbSession dbSession, OrganizationDto organization, long componentId) {
Optional<Long> qualityGateId = dbClient.projectQgateAssociationDao().selectQGateIdByComponentId(dbSession, componentId);
public QualityGateData getQualityGate(DbSession dbSession, OrganizationDto organization, ComponentDto component) {
Optional<Long> qualityGateId = dbClient.projectQgateAssociationDao().selectQGateIdByComponentId(dbSession, component.getId());
if (qualityGateId.isPresent()) {
QualityGateDto qualityGate = checkFound(dbClient.qualityGateDao().selectById(dbSession, qualityGateId.get()), "No quality gate has been found for id %s", qualityGateId);
return new QualityGateData(qualityGate, false);
} else {
QualityGateDto defaultQualityGate = dbClient.qualityGateDao().selectByOrganizationAndUuid(dbSession, organization, organization.getDefaultQualityGateUuid());
checkState(defaultQualityGate != null, "Unable to find the quality gate [%s] for organization [%s]", organization.getDefaultQualityGateUuid(), organization.getUuid());
return new QualityGateData(defaultQualityGate, true);
}
QualityGateDto defaultQualityGate = dbClient.qualityGateDao().selectByOrganizationAndUuid(dbSession, organization, organization.getDefaultQualityGateUuid());
checkState(defaultQualityGate != null, "Unable to find the quality gate [%s] for organization [%s]", organization.getDefaultQualityGateUuid(), organization.getUuid());
return new QualityGateData(defaultQualityGate, true);
}

public QGateWithOrgDto getByOrganizationAndId(DbSession dbSession, OrganizationDto organization, long qualityGateId) {

+ 1
- 0
server/sonar-server/src/main/java/org/sonar/server/qualitygate/QualityGateModule.java Visa fil

@@ -45,6 +45,7 @@ public class QualityGateModule extends Module {
QualityGateUpdater.class,
QualityGateConditionsUpdater.class,
QualityGateFinder.class,
QualityGateEvaluatorImpl.class,
// WS
QualityGatesWsSupport.class,
QualityGatesWs.class,

+ 2
- 2
server/sonar-server/src/main/java/org/sonar/server/qualitygate/RegisterQualityGates.java Visa fil

@@ -39,7 +39,7 @@ import org.sonar.db.qualitygate.QualityGateConditionDao;
import org.sonar.db.qualitygate.QualityGateConditionDto;
import org.sonar.db.qualitygate.QualityGateDao;
import org.sonar.db.qualitygate.QualityGateDto;
import org.sonar.server.computation.task.projectanalysis.qualitymodel.RatingGrid;
import org.sonar.server.computation.task.projectanalysis.qualitymodel.Rating;

import static java.util.Arrays.asList;
import static java.util.stream.Collectors.toMap;
@@ -57,7 +57,7 @@ public class RegisterQualityGates implements Startable {

private static final String BUILTIN_QUALITY_GATE_NAME = "Sonar way";
private static final int LEAK_PERIOD = 1;
private static final String A_RATING = Integer.toString(RatingGrid.Rating.A.getIndex());
private static final String A_RATING = Integer.toString(Rating.A.getIndex());

private static final List<QualityGateCondition> QUALITY_GATE_CONDITIONS = asList(
new QualityGateCondition().setMetricKey(NEW_SECURITY_RATING_KEY).setOperator(OPERATOR_GREATER_THAN).setPeriod(LEAK_PERIOD).setErrorThreshold(A_RATING),

+ 6
- 0
server/sonar-server/src/main/java/org/sonar/server/qualitygate/ShortLivingBranchQualityGate.java Visa fil

@@ -20,6 +20,7 @@
package org.sonar.server.qualitygate;

import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableSet;
import java.util.List;
import javax.annotation.CheckForNull;
import org.sonar.api.measures.CoreMetrics;
@@ -37,6 +38,11 @@ public final class ShortLivingBranchQualityGate {
new Condition(CoreMetrics.VULNERABILITIES_KEY, OPERATOR_GREATER_THAN, "0", false),
new Condition(CoreMetrics.CODE_SMELLS_KEY, OPERATOR_GREATER_THAN, "0", false));

public static final QualityGate GATE = new QualityGate(String.valueOf(ID), NAME, ImmutableSet.of(
new org.sonar.server.qualitygate.Condition(CoreMetrics.BUGS_KEY, org.sonar.server.qualitygate.Condition.Operator.GREATER_THAN, "0", null, false),
new org.sonar.server.qualitygate.Condition(CoreMetrics.VULNERABILITIES_KEY, org.sonar.server.qualitygate.Condition.Operator.GREATER_THAN, "0", null, false),
new org.sonar.server.qualitygate.Condition(CoreMetrics.CODE_SMELLS_KEY, org.sonar.server.qualitygate.Condition.Operator.GREATER_THAN, "0", null, false)));

private ShortLivingBranchQualityGate() {
// prevents instantiation
}

+ 0
- 138
server/sonar-server/src/main/java/org/sonar/server/qualitygate/changeevent/QGChangeEventFactory.java Visa fil

@@ -1,138 +0,0 @@
/*
* 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.qualitygate.changeevent;

import com.google.common.collect.ImmutableList;
import java.util.List;
import java.util.Objects;
import java.util.Optional;
import javax.annotation.Nullable;
import org.sonar.api.rules.RuleType;
import org.sonar.core.issue.DefaultIssue;
import org.sonar.core.issue.IssueChangeContext;
import org.sonar.db.component.ComponentDto;
import org.sonar.server.issue.ws.SearchResponseData;

import static com.google.common.base.Preconditions.checkArgument;

public interface QGChangeEventFactory {
/**
* Will call webhooks once for any short living branch which has at least one issue in {@link SearchResponseData} and
* if change described in {@link IssueChange} can alter the status of the short living branch.
*/
List<QGChangeEvent> from(IssueChangeData issueChangeData, IssueChange issueChange, IssueChangeContext context);

final class IssueChange {
private final RuleType ruleType;
private final String transitionKey;

public IssueChange(RuleType ruleType) {
this(ruleType, null);
}

public IssueChange(String transitionKey) {
this(null, transitionKey);
}

public IssueChange(@Nullable RuleType ruleType, @Nullable String transitionKey) {
checkArgument(ruleType != null || transitionKey != null, "At least one of ruleType and transitionKey must be non null");
this.ruleType = ruleType;
this.transitionKey = transitionKey;
}

public Optional<RuleType> getRuleType() {
return Optional.ofNullable(ruleType);
}

public Optional<String> getTransitionKey() {
return Optional.ofNullable(transitionKey);
}

@Override
public boolean equals(Object o) {
if (this == o) {
return true;
}
if (o == null || getClass() != o.getClass()) {
return false;
}
IssueChange that = (IssueChange) o;
return ruleType == that.ruleType &&
Objects.equals(transitionKey, that.transitionKey);
}

@Override
public int hashCode() {
return Objects.hash(ruleType, transitionKey);
}

@Override
public String toString() {
return "IssueChange{" +
"ruleType=" + ruleType +
", transitionKey='" + transitionKey + '\'' +
'}';
}
}

final class IssueChangeData {
private final List<DefaultIssue> issues;
private final List<ComponentDto> components;

public IssueChangeData(List<DefaultIssue> issues, List<ComponentDto> components) {
this.issues = ImmutableList.copyOf(issues);
this.components = ImmutableList.copyOf(components);
}

public List<DefaultIssue> getIssues() {
return issues;
}

public List<ComponentDto> getComponents() {
return components;
}

@Override
public boolean equals(Object o) {
if (this == o) {
return true;
}
if (o == null || getClass() != o.getClass()) {
return false;
}
IssueChangeData that = (IssueChangeData) o;
return Objects.equals(issues, that.issues) &&
Objects.equals(components, that.components);
}

@Override
public int hashCode() {
return Objects.hash(issues, components);
}

@Override
public String toString() {
return "IssueChangeData{" +
"issues=" + issues +
", components=" + components +
'}';
}
}
}

+ 0
- 156
server/sonar-server/src/main/java/org/sonar/server/qualitygate/changeevent/QGChangeEventFactoryImpl.java Visa fil

@@ -1,156 +0,0 @@
/*
* 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.qualitygate.changeevent;

import com.google.common.collect.ImmutableSet;
import com.google.common.collect.Sets;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import java.util.Set;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import org.sonar.api.config.Configuration;
import org.sonar.api.issue.DefaultTransitions;
import org.sonar.core.issue.DefaultIssue;
import org.sonar.core.issue.IssueChangeContext;
import org.sonar.core.util.stream.MoreCollectors;
import org.sonar.db.DbClient;
import org.sonar.db.DbSession;
import org.sonar.db.component.BranchDto;
import org.sonar.db.component.BranchType;
import org.sonar.db.component.ComponentDto;
import org.sonar.db.component.SnapshotDto;
import org.sonar.server.qualitygate.LiveQualityGateFactory;
import org.sonar.server.settings.ProjectConfigurationLoader;

import static java.util.Collections.emptyList;
import static org.sonar.core.util.stream.MoreCollectors.toSet;
import static org.sonar.core.util.stream.MoreCollectors.uniqueIndex;

public class QGChangeEventFactoryImpl implements QGChangeEventFactory {
private static final Set<String> MEANINGFUL_TRANSITIONS = ImmutableSet.of(
DefaultTransitions.RESOLVE, DefaultTransitions.FALSE_POSITIVE, DefaultTransitions.WONT_FIX, DefaultTransitions.REOPEN);
private final DbClient dbClient;
private final ProjectConfigurationLoader projectConfigurationLoader;
private final LiveQualityGateFactory liveQualityGateFactory;

public QGChangeEventFactoryImpl(DbClient dbClient, ProjectConfigurationLoader projectConfigurationLoader,
LiveQualityGateFactory liveQualityGateFactory) {
this.dbClient = dbClient;
this.projectConfigurationLoader = projectConfigurationLoader;
this.liveQualityGateFactory = liveQualityGateFactory;
}

@Override
public List<QGChangeEvent> from(IssueChangeData issueChangeData, IssueChange issueChange, IssueChangeContext context) {
if (isEmpty(issueChangeData) || !isUserChangeContext(context) || !isRelevant(issueChange)) {
return emptyList();
}

return from(issueChangeData);
}

private static boolean isRelevant(IssueChange issueChange) {
return issueChange.getTransitionKey().map(QGChangeEventFactoryImpl::isMeaningfulTransition).orElse(true);
}

private static boolean isEmpty(IssueChangeData issueChangeData) {
return issueChangeData.getIssues().isEmpty();
}

private static boolean isUserChangeContext(IssueChangeContext context) {
return context.login() != null;
}

private static boolean isMeaningfulTransition(String transitionKey) {
return MEANINGFUL_TRANSITIONS.contains(transitionKey);
}

private List<QGChangeEvent> from(IssueChangeData issueChangeData) {
try (DbSession dbSession = dbClient.openSession(false)) {
Map<String, ComponentDto> branchesByUuid = getBranchComponents(dbSession, issueChangeData);
if (branchesByUuid.isEmpty()) {
return emptyList();
}

Set<String> branchProjectUuids = branchesByUuid.values().stream()
.map(ComponentDto::uuid)
.collect(toSet(branchesByUuid.size()));
Set<BranchDto> shortBranches = dbClient.branchDao().selectByUuids(dbSession, branchProjectUuids)
.stream()
.filter(branchDto -> branchDto.getBranchType() == BranchType.SHORT)
.collect(toSet(branchesByUuid.size()));
if (shortBranches.isEmpty()) {
return emptyList();
}

Map<String, Configuration> configurationByUuid = projectConfigurationLoader.loadProjectConfigurations(dbSession,
shortBranches.stream().map(shortBranch -> branchesByUuid.get(shortBranch.getUuid())).collect(Collectors.toSet()));
Set<String> shortBranchesComponentUuids = shortBranches.stream().map(BranchDto::getUuid).collect(toSet(shortBranches.size()));
Map<String, SnapshotDto> analysisByProjectUuid = dbClient.snapshotDao().selectLastAnalysesByRootComponentUuids(
dbSession,
shortBranchesComponentUuids)
.stream()
.collect(uniqueIndex(SnapshotDto::getComponentUuid));

return shortBranches
.stream()
.map(shortBranch -> {
ComponentDto branch = branchesByUuid.get(shortBranch.getUuid());
SnapshotDto analysis = analysisByProjectUuid.get(shortBranch.getUuid());
if (branch != null && analysis != null) {
Configuration configuration = configurationByUuid.get(shortBranch.getUuid());

return new QGChangeEvent(branch, shortBranch, analysis, configuration,
() -> Optional.of(liveQualityGateFactory.buildForShortLivedBranch(branch)));
}
return null;
})
.filter(Objects::nonNull)
.collect(MoreCollectors.toList(shortBranches.size()));
}
}

private Map<String, ComponentDto> getBranchComponents(DbSession dbSession, IssueChangeData issueChangeData) {
Set<String> projectUuids = issueChangeData.getIssues().stream()
.map(DefaultIssue::projectUuid)
.collect(toSet());
Set<String> missingProjectUuids = ImmutableSet.copyOf(Sets.difference(
projectUuids,
issueChangeData.getComponents()
.stream()
.map(ComponentDto::uuid)
.collect(Collectors.toSet())));
if (missingProjectUuids.isEmpty()) {
return issueChangeData.getComponents()
.stream()
.filter(c -> projectUuids.contains(c.uuid()))
.filter(componentDto -> componentDto.getMainBranchProjectUuid() != null)
.collect(uniqueIndex(ComponentDto::uuid));
}
return Stream.concat(
issueChangeData.getComponents().stream().filter(c -> projectUuids.contains(c.uuid())),
dbClient.componentDao().selectByUuids(dbSession, missingProjectUuids).stream())
.filter(componentDto -> componentDto.getMainBranchProjectUuid() != null)
.collect(uniqueIndex(ComponentDto::uuid));
}
}

+ 3
- 1
server/sonar-server/src/main/java/org/sonar/server/qualitygate/changeevent/QGChangeEventListeners.java Visa fil

@@ -20,8 +20,10 @@
package org.sonar.server.qualitygate.changeevent;

import java.util.Collection;
import java.util.List;
import org.sonar.core.issue.DefaultIssue;

public interface QGChangeEventListeners {

void broadcastOnIssueChange(QGChangeEventFactory.IssueChangeData issueChangeData, Collection<QGChangeEvent> qgChangeEvents);
void broadcastOnIssueChange(List<DefaultIssue> changedIssues, Collection<QGChangeEvent> qgChangeEvents);
}

+ 4
- 3
server/sonar-server/src/main/java/org/sonar/server/qualitygate/changeevent/QGChangeEventListenersImpl.java Visa fil

@@ -22,6 +22,7 @@ package org.sonar.server.qualitygate.changeevent;
import com.google.common.collect.Multimap;
import java.util.Arrays;
import java.util.Collection;
import java.util.List;
import java.util.Set;
import org.sonar.api.rules.RuleType;
import org.sonar.api.utils.log.Logger;
@@ -57,15 +58,15 @@ public class QGChangeEventListenersImpl implements QGChangeEventListeners {
}

@Override
public void broadcastOnIssueChange(QGChangeEventFactory.IssueChangeData issueChangeData, Collection<QGChangeEvent> changeEvents) {
if (listeners.length == 0 || issueChangeData.getComponents().isEmpty() || issueChangeData.getIssues().isEmpty() || changeEvents.isEmpty()) {
public void broadcastOnIssueChange(List<DefaultIssue> issues, Collection<QGChangeEvent> changeEvents) {
if (listeners.length == 0 || issues.isEmpty() || changeEvents.isEmpty()) {
return;
}

try {
Multimap<String, QGChangeEvent> eventsByComponentUuid = changeEvents.stream()
.collect(MoreCollectors.index(t -> t.getProject().uuid()));
Multimap<String, DefaultIssue> issueByComponentUuid = issueChangeData.getIssues().stream()
Multimap<String, DefaultIssue> issueByComponentUuid = issues.stream()
.collect(MoreCollectors.index(DefaultIssue::projectUuid));

issueByComponentUuid.asMap()

+ 1
- 1
server/sonar-server/src/main/java/org/sonar/server/qualitygate/ws/GetByProjectAction.java Visa fil

@@ -99,7 +99,7 @@ public class GetByProjectAction implements QualityGatesWsAction {
throw insufficientPrivilegesException();
}

QualityGateData data = qualityGateFinder.getQualityGate(dbSession, organization, project.getId());
QualityGateData data = qualityGateFinder.getQualityGate(dbSession, organization, project);

writeProtobuf(buildResponse(data), request, response);
}

+ 9
- 0
server/sonar-server/src/main/java/org/sonar/server/settings/ProjectConfigurationLoader.java Visa fil

@@ -19,12 +19,16 @@
*/
package org.sonar.server.settings;

import java.util.Collections;
import java.util.Map;
import java.util.Set;
import org.sonar.api.config.Configuration;
import org.sonar.db.DbSession;
import org.sonar.db.component.ComponentDto;

import static java.lang.String.format;
import static java.util.Objects.requireNonNull;

public interface ProjectConfigurationLoader {
/**
* Loads configuration for the specified components.
@@ -37,4 +41,9 @@ public interface ProjectConfigurationLoader {
* Any component is accepted but SQ only supports specific properties for projects and branches.
*/
Map<String, Configuration> loadProjectConfigurations(DbSession dbSession, Set<ComponentDto> projects);

default Configuration loadProjectConfiguration(DbSession dbSession, ComponentDto project) {
Map<String, Configuration> configurations = loadProjectConfigurations(dbSession, Collections.singleton(project));
return requireNonNull(configurations.get(project.uuid()), () -> format("Configuration for project '%s' is not found", project.getKey()));
}
}

+ 2
- 3
server/sonar-server/src/main/java/org/sonar/server/test/index/TestIndexer.java Visa fil

@@ -93,12 +93,11 @@ public class TestIndexer implements ProjectIndexer {
switch (cause) {
case PROJECT_CREATION:
// no tests at that time
return emptyList();

case MEASURE_CHANGE:
case PROJECT_KEY_UPDATE:
case PROJECT_TAGS_UPDATE:
case PERMISSION_CHANGE:
// project key, tags and permissions are not part of tests/test
// Measures, project key, tags and permissions are not part of tests/test
return emptyList();

case PROJECT_DELETION:

+ 1
- 1
server/sonar-server/src/main/java/org/sonar/server/ui/ws/ComponentAction.java Visa fil

@@ -207,7 +207,7 @@ public class ComponentAction implements NavigationWsAction {
}

private void writeQualityGate(JsonWriter json, DbSession session, OrganizationDto organization, ComponentDto component) {
QualityGateFinder.QualityGateData qualityGateData = qualityGateFinder.getQualityGate(session, organization, component.getId());
QualityGateFinder.QualityGateData qualityGateData = qualityGateFinder.getQualityGate(session, organization, component);
QualityGateDto qualityGateDto = qualityGateData.getQualityGate();
json.name("qualityGate").beginObject()
.prop("key", qualityGateDto.getId())

+ 7
- 4
server/sonar-server/src/main/java/org/sonar/server/webhook/WebhookQGChangeEventListener.java Visa fil

@@ -22,6 +22,7 @@ package org.sonar.server.webhook;
import java.util.Map;
import java.util.Set;
import java.util.stream.Collectors;
import org.apache.commons.lang.StringUtils;
import org.sonar.db.DbClient;
import org.sonar.db.DbSession;
import org.sonar.db.component.AnalysisPropertyDto;
@@ -30,6 +31,7 @@ import org.sonar.db.component.ComponentDto;
import org.sonar.db.component.SnapshotDto;
import org.sonar.server.qualitygate.changeevent.QGChangeEvent;
import org.sonar.server.qualitygate.changeevent.QGChangeEventListener;
import org.sonar.server.webhook.Branch.Type;

public class WebhookQGChangeEventListener implements QGChangeEventListener {
private final WebHooks webhooks;
@@ -61,17 +63,18 @@ public class WebhookQGChangeEventListener implements QGChangeEventListener {
}

private WebhookPayload buildWebHookPayload(DbSession dbSession, QGChangeEvent event) {
ComponentDto branch = event.getProject();
BranchDto shortBranch = event.getBranch();
ComponentDto project = event.getProject();
BranchDto branch = event.getBranch();
SnapshotDto analysis = event.getAnalysis();
Map<String, String> analysisProperties = dbClient.analysisPropertiesDao().selectBySnapshotUuid(dbSession, analysis.getUuid())
.stream()
.collect(Collectors.toMap(AnalysisPropertyDto::getKey, AnalysisPropertyDto::getValue));
String projectUuid = StringUtils.defaultString(project.getMainBranchProjectUuid(), project.projectUuid());
ProjectAnalysis projectAnalysis = new ProjectAnalysis(
new Project(branch.getMainBranchProjectUuid(), branch.getKey(), branch.name()),
new Project(projectUuid, project.getKey(), project.name()),
null,
new Analysis(analysis.getUuid(), analysis.getCreatedAt()),
new Branch(false, shortBranch.getKey(), Branch.Type.SHORT),
new Branch(branch.isMain(), branch.getKey(), Type.valueOf(branch.getBranchType().name())),
event.getQualityGateSupplier().get().orElse(null),
null,
analysisProperties);

+ 5
- 10
server/sonar-server/src/main/java/org/sonar/server/webhook/ws/WebhookWsSupport.java Visa fil

@@ -19,6 +19,7 @@
*/
package org.sonar.server.webhook.ws;

import org.sonar.core.util.Protobuf;
import org.sonar.db.component.ComponentDto;
import org.sonar.db.webhook.WebhookDeliveryDto;
import org.sonar.db.webhook.WebhookDeliveryLiteDto;
@@ -39,23 +40,17 @@ class WebhookWsSupport {
.setName(dto.getName())
.setUrl(dto.getUrl())
.setSuccess(dto.isSuccess())
.setCeTaskId(dto.getCeTaskUuid())
.setComponentKey(component.getDbKey());
if (dto.getHttpStatus() != null) {
builder.setHttpStatus(dto.getHttpStatus());
}
if (dto.getDurationMs() != null) {
builder.setDurationMs(dto.getDurationMs());
}
Protobuf.setNullable(dto.getCeTaskUuid(), builder::setCeTaskId);
Protobuf.setNullable(dto.getHttpStatus(), builder::setHttpStatus);
Protobuf.setNullable(dto.getDurationMs(), builder::setDurationMs);
return builder;
}

static Webhooks.Delivery.Builder copyDtoToProtobuf(ComponentDto component, WebhookDeliveryDto dto, Webhooks.Delivery.Builder builder) {
copyDtoToProtobuf(component, (WebhookDeliveryLiteDto) dto, builder);
builder.setPayload(dto.getPayload());
if (dto.getErrorStacktrace() != null) {
builder.setErrorStacktrace(dto.getErrorStacktrace());
}
Protobuf.setNullable(dto.getErrorStacktrace(), builder::setErrorStacktrace);
return builder;
}
}

+ 5
- 5
server/sonar-server/src/test/java/org/sonar/server/computation/task/projectanalysis/formula/counter/RatingValueTest.java Visa fil

@@ -20,13 +20,13 @@
package org.sonar.server.computation.task.projectanalysis.formula.counter;

import org.junit.Test;
import org.sonar.server.computation.task.projectanalysis.qualitymodel.RatingGrid.Rating;
import org.sonar.server.computation.task.projectanalysis.qualitymodel.Rating;

import static org.assertj.core.api.Assertions.assertThat;
import static org.sonar.server.computation.task.projectanalysis.qualitymodel.RatingGrid.Rating.A;
import static org.sonar.server.computation.task.projectanalysis.qualitymodel.RatingGrid.Rating.B;
import static org.sonar.server.computation.task.projectanalysis.qualitymodel.RatingGrid.Rating.C;
import static org.sonar.server.computation.task.projectanalysis.qualitymodel.RatingGrid.Rating.D;
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;

public class RatingValueTest {


+ 2
- 2
server/sonar-server/src/test/java/org/sonar/server/computation/task/projectanalysis/measure/BestValueOptimizationTest.java Visa fil

@@ -28,8 +28,8 @@ import org.sonar.server.computation.task.projectanalysis.metric.MetricImpl;

import static org.assertj.core.api.Assertions.assertThat;
import static org.sonar.server.computation.task.projectanalysis.measure.Measure.newMeasureBuilder;
import static org.sonar.server.computation.task.projectanalysis.qualitymodel.RatingGrid.Rating.A;
import static org.sonar.server.computation.task.projectanalysis.qualitymodel.RatingGrid.Rating.B;
import static org.sonar.server.computation.task.projectanalysis.qualitymodel.Rating.A;
import static org.sonar.server.computation.task.projectanalysis.qualitymodel.Rating.B;

public class BestValueOptimizationTest {


+ 11
- 5
server/sonar-server/src/test/java/org/sonar/server/computation/task/projectanalysis/qualitygate/MutableQualityGateHolderRule.java Visa fil

@@ -19,8 +19,9 @@
*/
package org.sonar.server.computation.task.projectanalysis.qualitygate;

import com.google.common.base.Optional;
import java.util.Optional;
import org.junit.rules.ExternalResource;
import org.sonar.server.qualitygate.EvaluatedQualityGate;

public class MutableQualityGateHolderRule extends ExternalResource implements MutableQualityGateHolder {
private MutableQualityGateHolder delegate = new QualityGateHolderImpl();
@@ -31,13 +32,18 @@ public class MutableQualityGateHolderRule extends ExternalResource implements Mu
}

@Override
public void setNoQualityGate() {
delegate.setNoQualityGate();
public Optional<QualityGate> getQualityGate() {
return delegate.getQualityGate();
}

@Override
public Optional<QualityGate> getQualityGate() {
return delegate.getQualityGate();
public void setEvaluation(EvaluatedQualityGate evaluation) {
delegate.setEvaluation(evaluation);
}

@Override
public Optional<EvaluatedQualityGate> getEvaluation() {
return delegate.getEvaluation();
}

@Override

+ 27
- 6
server/sonar-server/src/test/java/org/sonar/server/computation/task/projectanalysis/qualitygate/QualityGateHolderImplTest.java Visa fil

@@ -19,15 +19,16 @@
*/
package org.sonar.server.computation.task.projectanalysis.qualitygate;

import java.util.Collections;
import org.junit.Test;
import org.sonar.server.qualitygate.EvaluatedQualityGate;

import static java.util.Collections.emptyList;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.guava.api.Assertions.assertThat;
import static org.mockito.Mockito.mock;

public class QualityGateHolderImplTest {

public static final QualityGate QUALITY_GATE = new QualityGate(4612, "name", Collections.<Condition>emptyList());
private static final QualityGate QUALITY_GATE = new QualityGate(4612, "name", emptyList());

@Test(expected = IllegalStateException.class)
public void getQualityGate_throws_ISE_if_QualityGate_not_set() {
@@ -56,13 +57,33 @@ public class QualityGateHolderImplTest {
assertThat(holder.getQualityGate().get()).isSameAs(QUALITY_GATE);
}

@Test(expected = IllegalStateException.class)
public void getEvaluation_throws_ISE_if_QualityGate_not_set() {
new QualityGateHolderImpl().getEvaluation();
}

@Test(expected = NullPointerException.class)
public void setEvaluation_throws_NPE_if_argument_is_null() {
new QualityGateHolderImpl().setEvaluation(null);
}

@Test(expected = IllegalStateException.class)
public void setEvaluation_throws_ISE_if_called_twice() {
QualityGateHolderImpl holder = new QualityGateHolderImpl();

EvaluatedQualityGate evaluation = mock(EvaluatedQualityGate.class);
holder.setEvaluation(evaluation);
holder.setEvaluation(evaluation);
}

@Test
public void getQualityGate_returns_absent_if_holder_initialized_with_setNoQualityGate() {
public void getEvaluation_returns_QualityGate_set_by_setQualityGate() {
QualityGateHolderImpl holder = new QualityGateHolderImpl();

holder.setNoQualityGate();
EvaluatedQualityGate evaluation = mock(EvaluatedQualityGate.class);
holder.setEvaluation(evaluation);

assertThat(holder.getQualityGate()).isAbsent();
assertThat(holder.getEvaluation().get()).isSameAs(evaluation);
}

}

+ 17
- 2
server/sonar-server/src/test/java/org/sonar/server/computation/task/projectanalysis/qualitygate/QualityGateHolderRule.java Visa fil

@@ -19,17 +19,21 @@
*/
package org.sonar.server.computation.task.projectanalysis.qualitygate;

import com.google.common.base.Optional;
import java.util.Optional;
import javax.annotation.Nullable;
import org.junit.rules.ExternalResource;
import org.sonar.server.qualitygate.EvaluatedQualityGate;

import static com.google.common.base.Preconditions.checkState;

public class QualityGateHolderRule extends ExternalResource implements QualityGateHolder {
@Nullable
private Optional<QualityGate> qualityGate;
@Nullable
private Optional<EvaluatedQualityGate> evaluation;

public void setQualityGate(@Nullable QualityGate qualityGate) {
this.qualityGate = Optional.fromNullable(qualityGate);
this.qualityGate = Optional.ofNullable(qualityGate);
}

@Override
@@ -38,6 +42,16 @@ public class QualityGateHolderRule extends ExternalResource implements QualityGa
return qualityGate;
}

public void setEvaluation(@Nullable EvaluatedQualityGate e) {
this.evaluation = Optional.ofNullable(e);
}

@Override
public Optional<EvaluatedQualityGate> getEvaluation() {
checkState(evaluation != null, "EvaluatedQualityGate has not been initialized");
return evaluation;
}

@Override
protected void after() {
reset();
@@ -45,5 +59,6 @@ public class QualityGateHolderRule extends ExternalResource implements QualityGa

public void reset() {
this.qualityGate = null;
this.evaluation = null;
}
}

server/sonar-server/src/test/java/org/sonar/server/computation/task/projectanalysis/qualitymodel/RatingGridTest.java → server/sonar-server/src/test/java/org/sonar/server/computation/task/projectanalysis/qualitymodel/DebtRatingGridTest.java Visa fil

@@ -25,16 +25,16 @@ import org.junit.Test;
import org.junit.rules.ExpectedException;

import static org.assertj.core.api.Assertions.assertThat;
import static org.sonar.server.computation.task.projectanalysis.qualitymodel.RatingGrid.Rating;
import static org.sonar.server.computation.task.projectanalysis.qualitymodel.RatingGrid.Rating.A;
import static org.sonar.server.computation.task.projectanalysis.qualitymodel.RatingGrid.Rating.B;
import static org.sonar.server.computation.task.projectanalysis.qualitymodel.RatingGrid.Rating.C;
import static org.sonar.server.computation.task.projectanalysis.qualitymodel.RatingGrid.Rating.D;
import static org.sonar.server.computation.task.projectanalysis.qualitymodel.RatingGrid.Rating.E;

public class RatingGridTest {
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;

private RatingGrid ratingGrid;
public class DebtRatingGridTest {

private DebtRatingGrid ratingGrid;

@Rule
public ExpectedException throwable = ExpectedException.none();
@@ -42,7 +42,7 @@ public class RatingGridTest {
@Before
public void setUp() {
double[] gridValues = new double[] {0.1, 0.2, 0.5, 1};
ratingGrid = new RatingGrid(gridValues);
ratingGrid = new DebtRatingGrid(gridValues);
}

@Test

+ 0
- 0
server/sonar-server/src/test/java/org/sonar/server/computation/task/projectanalysis/qualitymodel/MaintainabilityMeasuresVisitorTest.java Visa fil


Vissa filer visades inte eftersom för många filer har ändrats

Laddar…
Avbryt
Spara