]> source.dussan.org Git - sonarqube.git/commitdiff
SONAR-5305 Return aggregation of rules and severities. Return snapshot date in periods
authorJulien Lancelot <julien.lancelot@sonarsource.com>
Mon, 19 May 2014 15:15:49 +0000 (17:15 +0200)
committerJulien Lancelot <julien.lancelot@sonarsource.com>
Mon, 19 May 2014 15:34:43 +0000 (17:34 +0200)
21 files changed:
sonar-core/src/main/java/org/sonar/core/issue/db/IssueDao.java
sonar-core/src/main/java/org/sonar/core/issue/db/IssueMapper.java
sonar-core/src/main/resources/org/sonar/core/issue/db/IssueMapper.xml
sonar-core/src/main/resources/org/sonar/core/issue/db/IssueStatsMapper.xml
sonar-core/src/test/java/org/sonar/core/issue/db/IssueDaoTest.java
sonar-core/src/test/resources/org/sonar/core/issue/db/IssueDaoTest/find_rules_by_component.xml [new file with mode: 0644]
sonar-core/src/test/resources/org/sonar/core/issue/db/IssueDaoTest/find_severities_by_component.xml [new file with mode: 0644]
sonar-core/src/test/resources/org/sonar/core/issue/db/IssueDaoTest/shared.xml
sonar-server/src/main/java/org/sonar/server/component/ws/ComponentAppAction.java
sonar-server/src/main/java/org/sonar/server/issue/IssueService.java
sonar-server/src/main/java/org/sonar/server/issue/RulesAggregation.java [new file with mode: 0644]
sonar-server/src/main/java/org/sonar/server/user/UserSession.java
sonar-server/src/main/resources/org/sonar/server/component/ws/components-app-example-show.json
sonar-server/src/test/java/org/sonar/server/component/ws/ComponentAppActionTest.java
sonar-server/src/test/java/org/sonar/server/component/ws/ComponentsWsTest.java
sonar-server/src/test/java/org/sonar/server/issue/IssueServiceTest.java
sonar-server/src/test/resources/org/sonar/server/component/ws/ComponentAppActionTest/app.json
sonar-server/src/test/resources/org/sonar/server/component/ws/ComponentAppActionTest/app_with_measures.json [new file with mode: 0644]
sonar-server/src/test/resources/org/sonar/server/component/ws/ComponentAppActionTest/app_with_periods.json [new file with mode: 0644]
sonar-server/src/test/resources/org/sonar/server/component/ws/ComponentAppActionTest/app_with_rules.json [new file with mode: 0644]
sonar-server/src/test/resources/org/sonar/server/component/ws/ComponentAppActionTest/app_with_severities.json [new file with mode: 0644]

index f566262858648bb0a23c4fe2958df06b1645af36..44bfdb9de5181cf8b514920d17710592d8bbf3b3 100644 (file)
@@ -28,6 +28,7 @@ import org.sonar.api.BatchComponent;
 import org.sonar.api.ServerComponent;
 import org.sonar.api.issue.IssueQuery;
 import org.sonar.core.persistence.MyBatis;
+import org.sonar.core.rule.RuleDto;
 
 import javax.annotation.CheckForNull;
 import javax.annotation.Nullable;
@@ -138,4 +139,24 @@ public class IssueDao implements BatchComponent, ServerComponent {
     }
     return dtosList;
   }
+
+  // TODO replace by aggregation in IssueIndex
+  public List<RuleDto> findRulesByComponent(String componentKey) {
+    SqlSession session = mybatis.openSession(false);
+    try {
+      return session.getMapper(IssueMapper.class).findRulesByComponent(componentKey);
+    } finally {
+      MyBatis.closeQuietly(session);
+    }
+  }
+
+  // TODO replace by aggregation in IssueIndex
+  public List<String> findSeveritiesByComponent(String componentKey) {
+    SqlSession session = mybatis.openSession(false);
+    try {
+      return session.getMapper(IssueMapper.class).findSeveritiesByComponent(componentKey);
+    } finally {
+      MyBatis.closeQuietly(session);
+    }
+  }
 }
index 600b5e4f8c2e0dab00d605a4df6b2c01944e2e32..7204be2b57249804dded8bfc989bacdd08c60524 100644 (file)
@@ -21,6 +21,7 @@ package org.sonar.core.issue.db;
 
 import org.apache.ibatis.annotations.Param;
 import org.sonar.api.issue.IssueQuery;
+import org.sonar.core.rule.RuleDto;
 
 import javax.annotation.Nullable;
 
@@ -47,6 +48,10 @@ public interface IssueMapper {
                                 @Nullable @Param("userId") Integer userId, @Nullable @Param("role") String role);
 
 
+  List<RuleDto> findRulesByComponent(String componentKey);
+
+  List<String> findSeveritiesByComponent(String componentKey);
+
   void insert(IssueDto issue);
 
   int update(IssueDto issue);
index 785410c1ae27a062469e5ab2b8e2d81c9a2d88d2..3180427721ccd235b3d22b876eb5c19a98b78187 100644 (file)
     </where>
   </sql>
 
+  <select id="findRulesByComponent" parameterType="String" resultType="Rule">
+    SELECT <include refid="org.sonar.core.rule.RuleMapper.selectColumns" />
+    FROM issues i
+    INNER JOIN projects p on p.id=i.component_id
+    INNER JOIN rules r on r.id=i.rule_id
+    <where>
+      AND p.kee=#{componentKey}
+      AND i.resolution IS NULL
+    </where>
+  </select>
+
+  <select id="findSeveritiesByComponent" parameterType="String" resultType="String">
+    SELECT i.severity
+    FROM issues i
+    INNER JOIN projects on projects.id=i.component_id
+    INNER JOIN rules on rules.id=i.rule_id
+    <where>
+      AND projects.kee=#{componentKey}
+      AND i.resolution IS NULL
+    </where>
+  </select>
+
 </mapper>
 
index 5369af451489ac0837ef4f61b78b7b0c08e3a1cf..a721832a98a2d42b497080038c3fa900b10877c7 100644 (file)
@@ -5,12 +5,12 @@
 <mapper namespace="org.sonar.core.issue.db.IssueStatsMapper">
 
   <select id="selectIssuesColumn" parameterType="map" resultType="Object">
-    select
+    SELECT
     <if test="'ASSIGNEE'.equals(column)">
       i.assignee
     </if>
-    from issues i
+    FROM issues i
     <include refid="org.sonar.core.issue.db.IssueMapper.selectQueryConditions"/>
   </select>
 
-</mapper>
\ No newline at end of file
+</mapper>
index 196b10204e05b7833dc2c1879112c0f8e7c53275..cf8dc4a3d3b3c7862179ab60303a3b6d375efdf1 100644 (file)
@@ -27,8 +27,10 @@ import org.junit.Before;
 import org.junit.Test;
 import org.sonar.api.issue.IssueQuery;
 import org.sonar.api.rule.RuleKey;
+import org.sonar.api.rule.Severity;
 import org.sonar.api.utils.DateUtils;
 import org.sonar.core.persistence.AbstractDaoTestCase;
+import org.sonar.core.rule.RuleDto;
 
 import java.util.List;
 
@@ -428,6 +430,22 @@ public class IssueDaoTest extends AbstractDaoTestCase {
     assertThat(issue.getRootComponentKey()).isEqualTo("struts");
   }
 
+  @Test
+  public void find_rules_by_component() {
+    setupData("shared", "find_rules_by_component");
+
+    List<RuleDto> results = dao.findRulesByComponent("Action.java");
+    assertThat(results).hasSize(3);
+  }
+
+  @Test
+  public void find_severities_by_component() {
+    setupData("shared", "find_severities_by_component");
+
+    List<String> results = dao.findSeveritiesByComponent("Action.java");
+    assertThat(results).containsExactly(Severity.BLOCKER, Severity.MAJOR, Severity.BLOCKER);
+  }
+
   private List<Long> getIssueIds(List<IssueDto> issues) {
     return newArrayList(Iterables.transform(issues, new Function<IssueDto, Long>() {
       @Override
diff --git a/sonar-core/src/test/resources/org/sonar/core/issue/db/IssueDaoTest/find_rules_by_component.xml b/sonar-core/src/test/resources/org/sonar/core/issue/db/IssueDaoTest/find_rules_by_component.xml
new file mode 100644 (file)
index 0000000..d396550
--- /dev/null
@@ -0,0 +1,80 @@
+<dataset>
+
+  <!-- rule 500 -->
+  <issues
+      id="100"
+      kee="ABCDE-1"
+      component_id="401"
+      root_component_id="399"
+      rule_id="500"
+      severity="BLOCKER"
+      manual_severity="[false]"
+      message="[null]"
+      line="200"
+      effort_to_fix="4.2"
+      status="OPEN"
+      resolution="[null]"
+      checksum="XXX"
+      reporter="arthur"
+      assignee="perceval"
+      author_login="[null]"
+      issue_attributes="JIRA=FOO-1234"
+      issue_creation_date="2013-04-16"
+      issue_update_date="2013-04-16"
+      issue_close_date="2013-04-16"
+      created_at="2013-04-16"
+      updated_at="2013-04-16"
+      />
+
+  <issues
+      id="101"
+      kee="ABCDE-2"
+      component_id="401"
+      root_component_id="399"
+      rule_id="500"
+      severity="BLOCKER"
+      manual_severity="[false]"
+      message="[null]"
+      line="200"
+      effort_to_fix="4.2"
+      status="OPEN"
+      resolution="[null]"
+      checksum="XXX"
+      reporter="arthur"
+      assignee="perceval"
+      author_login="[null]"
+      issue_attributes="JIRA=FOO-1234"
+      issue_creation_date="2013-04-16"
+      issue_update_date="2013-04-16"
+      issue_close_date="2013-04-16"
+      created_at="2013-04-16"
+      updated_at="2013-04-16"
+      />
+
+
+  <!-- rule 501 -->
+  <issues
+      id="102"
+      kee="ABCDE-3"
+      component_id="401"
+      root_component_id="399"
+      rule_id="501"
+      severity="BLOCKER"
+      manual_severity="[false]"
+      message="[null]"
+      line="200"
+      effort_to_fix="4.2"
+      status="OPEN"
+      resolution="[null]"
+      checksum="XXX"
+      reporter="arthur"
+      assignee="perceval"
+      author_login="[null]"
+      issue_attributes="JIRA=FOO-1234"
+      issue_creation_date="2013-04-16"
+      issue_update_date="2013-04-16"
+      issue_close_date="2013-04-16"
+      created_at="2013-04-16"
+      updated_at="2013-04-16"
+      />
+</dataset>
diff --git a/sonar-core/src/test/resources/org/sonar/core/issue/db/IssueDaoTest/find_severities_by_component.xml b/sonar-core/src/test/resources/org/sonar/core/issue/db/IssueDaoTest/find_severities_by_component.xml
new file mode 100644 (file)
index 0000000..fbadd1c
--- /dev/null
@@ -0,0 +1,100 @@
+<!--
+  ~ SonarQube, open source software quality management tool.
+  ~ Copyright (C) 2008-2014 SonarSource
+  ~ mailto:contact AT sonarsource DOT com
+  ~
+  ~ SonarQube is free software; you can redistribute it and/or
+  ~ modify it under the terms of the GNU Lesser General Public
+  ~ License as published by the Free Software Foundation; either
+  ~ version 3 of the License, or (at your option) any later version.
+  ~
+  ~ SonarQube is distributed in the hope that it will be useful,
+  ~ but WITHOUT ANY WARRANTY; without even the implied warranty of
+  ~ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+  ~ Lesser General Public License for more details.
+  ~
+  ~ You should have received a copy of the GNU Lesser General Public License
+  ~ along with this program; if not, write to the Free Software Foundation,
+  ~ Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+  -->
+
+<dataset>
+
+  <!-- rule 500 -->
+  <issues
+      id="100"
+      kee="ABCDE-1"
+      component_id="401"
+      root_component_id="399"
+      rule_id="500"
+      severity="BLOCKER"
+      manual_severity="[false]"
+      message="[null]"
+      line="200"
+      effort_to_fix="4.2"
+      status="OPEN"
+      resolution="[null]"
+      checksum="XXX"
+      reporter="arthur"
+      assignee="perceval"
+      author_login="[null]"
+      issue_attributes="JIRA=FOO-1234"
+      issue_creation_date="2013-04-16"
+      issue_update_date="2013-04-16"
+      issue_close_date="2013-04-16"
+      created_at="2013-04-16"
+      updated_at="2013-04-16"
+      />
+
+  <issues
+      id="101"
+      kee="ABCDE-2"
+      component_id="401"
+      root_component_id="399"
+      rule_id="500"
+      severity="MAJOR"
+      manual_severity="[false]"
+      message="[null]"
+      line="200"
+      effort_to_fix="4.2"
+      status="OPEN"
+      resolution="[null]"
+      checksum="XXX"
+      reporter="arthur"
+      assignee="perceval"
+      author_login="[null]"
+      issue_attributes="JIRA=FOO-1234"
+      issue_creation_date="2013-04-16"
+      issue_update_date="2013-04-16"
+      issue_close_date="2013-04-16"
+      created_at="2013-04-16"
+      updated_at="2013-04-16"
+      />
+
+
+  <!-- rule 501 -->
+  <issues
+      id="102"
+      kee="ABCDE-3"
+      component_id="401"
+      root_component_id="399"
+      rule_id="501"
+      severity="BLOCKER"
+      manual_severity="[false]"
+      message="[null]"
+      line="200"
+      effort_to_fix="4.2"
+      status="OPEN"
+      resolution="[null]"
+      checksum="XXX"
+      reporter="arthur"
+      assignee="perceval"
+      author_login="[null]"
+      issue_attributes="JIRA=FOO-1234"
+      issue_creation_date="2013-04-16"
+      issue_update_date="2013-04-16"
+      issue_close_date="2013-04-16"
+      created_at="2013-04-16"
+      updated_at="2013-04-16"
+      />
+</dataset>
index 1a7ef5f4246d2cd9804efdfe54f7c9991e5ecef7..fe24d237d6bae43a7ced1de10873698c1b110d11 100644 (file)
@@ -12,7 +12,7 @@
   <snapshots id="102" project_id="401" root_snapshot_id="100" parent_snapshot_id="101" root_project_id="399" path="100.101." islast="[true]" />
   <snapshots id="103" project_id="402" root_snapshot_id="100" parent_snapshot_id="101" root_project_id="399" path="100.101." islast="[true]" />
 
-  <rules tags="[null]" system_tags="[null]" id="500" plugin_rule_key="AvoidCycle" plugin_name="squid" language="java" />
-  <rules tags="[null]" system_tags="[null]" id="501" plugin_rule_key="NullRef" plugin_name="squid" language="xoo" />
+  <rules id="500" tags="[null]" system_tags="[null]" plugin_rule_key="AvoidCycle" plugin_name="squid" language="java" />
+  <rules id="501" tags="[null]" system_tags="[null]" plugin_rule_key="NullRef" plugin_name="squid" language="xoo" />
 
 </dataset>
index c21ff1afdcea00d657b5835cf1cd356175dd76d5..2535694121c127d1126a9d2e76fbcaea516ab0ac 100644 (file)
@@ -20,6 +20,7 @@
 
 package org.sonar.server.component.ws;
 
+import com.google.common.collect.Multiset;
 import com.google.common.io.Resources;
 import org.sonar.api.component.Component;
 import org.sonar.api.i18n.I18n;
@@ -29,6 +30,7 @@ import org.sonar.api.server.ws.Request;
 import org.sonar.api.server.ws.RequestHandler;
 import org.sonar.api.server.ws.Response;
 import org.sonar.api.server.ws.WebService;
+import org.sonar.api.utils.DateUtils;
 import org.sonar.api.utils.Durations;
 import org.sonar.api.utils.text.JsonWriter;
 import org.sonar.api.web.UserRole;
@@ -42,11 +44,14 @@ import org.sonar.core.resource.ResourceDao;
 import org.sonar.core.resource.SnapshotDto;
 import org.sonar.core.timemachine.Periods;
 import org.sonar.server.exceptions.NotFoundException;
+import org.sonar.server.issue.IssueService;
+import org.sonar.server.issue.RulesAggregation;
 import org.sonar.server.user.UserSession;
 
 import javax.annotation.CheckForNull;
 import javax.annotation.Nullable;
 
+import java.util.Date;
 import java.util.List;
 
 public class ComponentAppAction implements RequestHandler {
@@ -56,14 +61,16 @@ public class ComponentAppAction implements RequestHandler {
   private final ResourceDao resourceDao;
   private final MeasureDao measureDao;
   private final PropertiesDao propertiesDao;
+  private final IssueService issueService;
   private final Periods periods;
   private final Durations durations;
   private final I18n i18n;
 
-  public ComponentAppAction(ResourceDao resourceDao, MeasureDao measureDao, PropertiesDao propertiesDao, Periods periods, Durations durations, I18n i18n) {
+  public ComponentAppAction(ResourceDao resourceDao, MeasureDao measureDao, PropertiesDao propertiesDao, IssueService issueService, Periods periods, Durations durations, I18n i18n) {
     this.resourceDao = resourceDao;
     this.measureDao = measureDao;
     this.propertiesDao = propertiesDao;
+    this.issueService = issueService;
     this.periods = periods;
     this.durations = durations;
     this.i18n = i18n;
@@ -121,6 +128,7 @@ public class ComponentAppAction implements RequestHandler {
 
       json.prop("fav", isFavourite);
       appendPeriods(json, projectId);
+      appendRulesAggregation(json, component.key());
       appendMeasures(json, fileKey);
     }
     json.endObject();
@@ -149,9 +157,14 @@ public class ComponentAppAction implements RequestHandler {
       for (int i = 1; i <= 5; i++) {
         String mode = snapshotDto.getPeriodMode(i);
         if (mode != null) {
-          String label = periods.label(mode, snapshotDto.getPeriodModeParameter(i), snapshotDto.getPeriodDate(i));
+          Date periodDate = snapshotDto.getPeriodDate(i);
+          String label = periods.label(mode, snapshotDto.getPeriodModeParameter(i), periodDate);
           if (label != null) {
-            json.beginObject().prop(Integer.toString(i), label).endObject();
+            json.beginArray()
+              .value(i)
+              .value(label)
+              .value(periodDate != null ? DateUtils.formatDateTime(periodDate) : null)
+              .endArray();
           }
         }
       }
@@ -159,6 +172,30 @@ public class ComponentAppAction implements RequestHandler {
     json.endArray();
   }
 
+  private void appendRulesAggregation(JsonWriter json, String componentKey) {
+    json.name("severities").beginArray();
+    Multiset<String> severities = issueService.findSeveritiesByComponent(componentKey);
+    for (String severity : severities.elementSet()) {
+      json.beginArray()
+        .value(severity)
+        .value(i18n.message(UserSession.get().locale(), "severity." + severity, null))
+        .value(severities.count(severity))
+        .endArray();
+    }
+    json.endArray();
+
+    json.name("rules").beginArray();
+    RulesAggregation rulesAggregation = issueService.findRulesByComponent(componentKey);
+    for (RulesAggregation.Rule rule : rulesAggregation.rules()) {
+      json.beginArray()
+        .value(rule.ruleKey().toString())
+        .value(rule.name())
+        .value(rulesAggregation.countRule(rule))
+        .endArray();
+    }
+    json.endArray();
+  }
+
   @CheckForNull
   private Component componentById(@Nullable Long componentId) {
     if (componentId != null) {
index c15c3e10c1e244a580c40dac63e770868a7f3810..774bca951bee933fa0187498c7e68afec188b4ad 100644 (file)
@@ -21,6 +21,8 @@ package org.sonar.server.issue;
 
 import com.google.common.base.Objects;
 import com.google.common.base.Strings;
+import com.google.common.collect.HashMultiset;
+import com.google.common.collect.Multiset;
 import org.apache.commons.lang.StringUtils;
 import org.sonar.api.ServerComponent;
 import org.sonar.api.issue.ActionPlan;
@@ -39,6 +41,7 @@ import org.sonar.api.web.UserRole;
 import org.sonar.core.issue.DefaultIssueBuilder;
 import org.sonar.core.issue.IssueNotifications;
 import org.sonar.core.issue.IssueUpdater;
+import org.sonar.core.issue.db.IssueDao;
 import org.sonar.core.issue.db.IssueStorage;
 import org.sonar.core.issue.workflow.IssueWorkflow;
 import org.sonar.core.issue.workflow.Transition;
@@ -46,6 +49,7 @@ import org.sonar.core.preview.PreviewCache;
 import org.sonar.core.resource.ResourceDao;
 import org.sonar.core.resource.ResourceDto;
 import org.sonar.core.resource.ResourceQuery;
+import org.sonar.core.rule.RuleDto;
 import org.sonar.core.user.AuthorizationDao;
 import org.sonar.server.issue.actionplan.ActionPlanService;
 import org.sonar.server.user.UserSession;
@@ -67,21 +71,24 @@ public class IssueService implements ServerComponent {
   private final ActionPlanService actionPlanService;
   private final RuleFinder ruleFinder;
   private final ResourceDao resourceDao;
+  private final IssueDao issueDao;
   private final AuthorizationDao authorizationDao;
   private final UserFinder userFinder;
   private final PreviewCache dryRunCache;
 
+
   public IssueService(DefaultIssueFinder finder,
-    IssueWorkflow workflow,
-    IssueStorage issueStorage,
-    IssueUpdater issueUpdater,
-    IssueNotifications issueNotifications,
-    ActionPlanService actionPlanService,
-    RuleFinder ruleFinder,
-    ResourceDao resourceDao,
-    AuthorizationDao authorizationDao,
-    UserFinder userFinder,
-    PreviewCache dryRunCache) {
+                      IssueWorkflow workflow,
+                      IssueStorage issueStorage,
+                      IssueUpdater issueUpdater,
+                      IssueNotifications issueNotifications,
+                      ActionPlanService actionPlanService,
+                      RuleFinder ruleFinder,
+                      ResourceDao resourceDao,
+                      IssueDao issueDao,
+                      AuthorizationDao authorizationDao,
+                      UserFinder userFinder,
+                      PreviewCache dryRunCache) {
     this.finder = finder;
     this.workflow = workflow;
     this.issueStorage = issueStorage;
@@ -90,6 +97,7 @@ public class IssueService implements ServerComponent {
     this.ruleFinder = ruleFinder;
     this.issueNotifications = issueNotifications;
     this.resourceDao = resourceDao;
+    this.issueDao = issueDao;
     this.authorizationDao = authorizationDao;
     this.userFinder = userFinder;
     this.dryRunCache = dryRunCache;
@@ -243,7 +251,7 @@ public class IssueService implements ServerComponent {
     return issue;
   }
 
-  private Rule findRule (RuleKey ruleKey) {
+  private Rule findRule(RuleKey ruleKey) {
     Rule rule = ruleFinder.findByKey(ruleKey);
     if (rule == null) {
       throw new IllegalArgumentException("Unknown rule: " + ruleKey);
@@ -271,4 +279,22 @@ public class IssueService implements ServerComponent {
     }
   }
 
+  // TODO result should be replaced by an aggregation object in IssueIndex
+  public RulesAggregation findRulesByComponent(String componentKey) {
+    RulesAggregation rulesAggregation = new RulesAggregation();
+    for (RuleDto ruleDto : issueDao.findRulesByComponent(componentKey)) {
+      rulesAggregation.add(ruleDto);
+    }
+    return rulesAggregation;
+  }
+
+  // TODO result should be replaced by an aggregation object in IssueIndex
+  public Multiset<String> findSeveritiesByComponent(String componentKey) {
+    Multiset<String> aggregation = HashMultiset.create();
+    for (String severity : issueDao.findSeveritiesByComponent(componentKey)) {
+      aggregation.add(severity);
+    }
+    return aggregation;
+  }
+
 }
diff --git a/sonar-server/src/main/java/org/sonar/server/issue/RulesAggregation.java b/sonar-server/src/main/java/org/sonar/server/issue/RulesAggregation.java
new file mode 100644 (file)
index 0000000..433cfba
--- /dev/null
@@ -0,0 +1,102 @@
+/*
+ * SonarQube, open source software quality management tool.
+ * Copyright (C) 2008-2014 SonarSource
+ * mailto:contact AT sonarsource DOT com
+ *
+ * SonarQube is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * SonarQube is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+ */
+
+package org.sonar.server.issue;
+
+import com.google.common.collect.HashMultiset;
+import com.google.common.collect.Multiset;
+import org.sonar.api.rule.RuleKey;
+import org.sonar.core.rule.RuleDto;
+
+import java.util.Collection;
+
+public class RulesAggregation {
+
+  private Multiset<Rule> rules;
+
+  public RulesAggregation() {
+    this.rules = HashMultiset.create();
+  }
+
+  public RulesAggregation add(RuleDto ruleDto) {
+    rules.add(new Rule().setRuleKey(ruleDto.getKey()).setName(ruleDto.getName()));
+    return this;
+  }
+
+  public Collection<Rule> rules() {
+    return rules.elementSet();
+  }
+
+  public int countRule(Rule rule) {
+    return rules.count(rule);
+  }
+
+  public static class Rule {
+
+    private RuleKey ruleKey;
+    private String name;
+
+    public RuleKey ruleKey() {
+      return ruleKey;
+    }
+
+    public Rule setRuleKey(RuleKey ruleKey) {
+      this.ruleKey = ruleKey;
+      return this;
+    }
+
+    public String name() {
+      return name;
+    }
+
+    public Rule setName(String name) {
+      this.name = name;
+      return this;
+    }
+
+    @Override
+    public boolean equals(Object o) {
+      if (this == o) {
+        return true;
+      }
+      if (o == null || getClass() != o.getClass()) {
+        return false;
+      }
+
+      Rule rule = (Rule) o;
+
+      if (!name.equals(rule.name)) {
+        return false;
+      }
+      if (!ruleKey.equals(rule.ruleKey)) {
+        return false;
+      }
+
+      return true;
+    }
+
+    @Override
+    public int hashCode() {
+      int result = ruleKey.hashCode();
+      result = 31 * result + name.hashCode();
+      return result;
+    }
+  }
+}
index a7c4ee00a4e23660b6b98b3d6be5cb3d8b59f28e..cf3ffe6b7b4d690224236ad423c0de0b993216e0 100644 (file)
@@ -61,6 +61,7 @@ public class UserSession {
   List<String> globalPermissions = null;
 
   HashMultimap<String, String> projectKeyByPermission = HashMultimap.create();
+  HashMultimap<String, String> componentKeyByPermission = HashMultimap.create();
   List<String> projectPermissions = newArrayList();
 
   UserSession() {
index 83bf5229658d86f5d44a88ae8ed4d54c6982a72f..3ff72f9b0b0eed93112bb3b0c8f2b8146e6443f4 100644 (file)
@@ -7,20 +7,40 @@
   "projectName": "SonarQube",
   "fav": true,
   "periods": [
-    {"1" : "since previous analysis (May 08 2014)"},
-    {"2" : "over 365 days (May 17 2013)"},
-    {"3" : "since previous version (4.3 - Apr 17 2014)"}
+    [
+      1,
+      "since previous analysis (2014 May 08)",
+      "2014-05-08T23:40:12+0200"
+    ],
+    [
+      2,
+      "over 365 days (2013 May 17)",
+      "2013-05-17T23:52:45+0200"
+    ],
+    [
+      3,
+      "since previous version (4.3 - 2014 Apr 17)",
+      "2014-04-17T23:34:08+0200"
+    ]
+  ],
+  "severities": [
+    [
+      "INFO",
+      "Info",
+      4
+    ]
+  ],
+  "rules": [
+    [
+      "squid:S1133",
+      "Deprecated code should be removed eventually",
+      4
+    ]
   ],
   "measures": {
-    "fNcloc": "200",
-    "fCoverage": "95.4%",
-    "fDuplicationDensity": "7.4%",
-    "fDebt": "3d 2h",
-    "fIssues": "14",
-    "fBlockerIssues": "1",
-    "fCriticalIssues": "2",
-    "fMajorIssues": "5",
-    "fMinorIssues": "4",
-    "fInfoIssues": "2"
+    "fNcloc": "12",
+    "fDebt": "4h",
+    "fIssues": "4",
+    "fInfoIssues": "4"
   }
 }
index 4b1720ec673d42bdd356d8d7dfac0bc26970ddad..24d5adc52f9a93443aba99d0d75f7c4cde02fe64 100644 (file)
@@ -20,6 +20,8 @@
 
 package org.sonar.server.component.ws;
 
+import com.google.common.collect.HashMultiset;
+import com.google.common.collect.Multiset;
 import org.junit.Before;
 import org.junit.Test;
 import org.junit.runner.RunWith;
@@ -27,6 +29,7 @@ import org.mockito.Mock;
 import org.mockito.runners.MockitoJUnitRunner;
 import org.sonar.api.i18n.I18n;
 import org.sonar.api.measures.CoreMetrics;
+import org.sonar.api.utils.DateUtils;
 import org.sonar.api.utils.Duration;
 import org.sonar.api.utils.Durations;
 import org.sonar.api.web.UserRole;
@@ -38,7 +41,10 @@ import org.sonar.core.properties.PropertyDto;
 import org.sonar.core.properties.PropertyQuery;
 import org.sonar.core.resource.ResourceDao;
 import org.sonar.core.resource.SnapshotDto;
+import org.sonar.core.rule.RuleDto;
 import org.sonar.core.timemachine.Periods;
+import org.sonar.server.issue.IssueService;
+import org.sonar.server.issue.RulesAggregation;
 import org.sonar.server.user.MockUserSession;
 import org.sonar.server.ws.WsTester;
 
@@ -47,6 +53,7 @@ import java.util.Locale;
 
 import static com.google.common.collect.Lists.newArrayList;
 import static org.mockito.Matchers.*;
+import static org.mockito.Mockito.mock;
 import static org.mockito.Mockito.when;
 
 @RunWith(MockitoJUnitRunner.class)
@@ -64,6 +71,9 @@ public class ComponentAppActionTest {
   @Mock
   PropertiesDao propertiesDao;
 
+  @Mock
+  IssueService issueService;
+
   @Mock
   Periods periods;
 
@@ -77,7 +87,10 @@ public class ComponentAppActionTest {
 
   @Before
   public void setUp() throws Exception {
-    tester = new WsTester(new ComponentsWs(new ComponentAppAction(resourceDao, measureDao, propertiesDao, periods, durations, i18n)));
+    when(issueService.findSeveritiesByComponent(COMPONENT_KEY)).thenReturn(mock(Multiset.class));
+    when(issueService.findRulesByComponent(COMPONENT_KEY)).thenReturn(mock(RulesAggregation.class));
+
+    tester = new WsTester(new ComponentsWs(new ComponentAppAction(resourceDao, measureDao, propertiesDao, issueService, periods, durations, i18n)));
   }
 
   @Test
@@ -91,6 +104,16 @@ public class ComponentAppActionTest {
     when(resourceDao.findById(1L)).thenReturn(new ComponentDto().setId(1L).setLongName("SonarQube"));
     when(propertiesDao.selectByQuery(any(PropertyQuery.class))).thenReturn(newArrayList(new PropertyDto()));
 
+    WsTester.TestRequest request = tester.newGetRequest("api/components", "app").setParam("key", COMPONENT_KEY);
+    request.execute().assertJson(getClass(), "app.json");
+  }
+
+  @Test
+  public void app_with_measures() throws Exception {
+    MockUserSession.set().addProjectPermissions(UserRole.CODEVIEWER, PROJECT_KEY).addComponent(COMPONENT_KEY, PROJECT_KEY);
+
+    addProjectSample();
+
     addMeasure(CoreMetrics.NCLOC_KEY, 200);
     addMeasure(CoreMetrics.COVERAGE_KEY, 95.4);
     addMeasure(CoreMetrics.DUPLICATED_LINES_DENSITY_KEY, 7.4);
@@ -104,19 +127,68 @@ public class ComponentAppActionTest {
     when(measureDao.findByComponentKeyAndMetricKey(COMPONENT_KEY, CoreMetrics.TECHNICAL_DEBT_KEY)).thenReturn(new MeasureDto().setValue(182.0));
     when(durations.format(any(Locale.class), any(Duration.class), eq(Durations.DurationFormat.SHORT))).thenReturn("3h 2min");
 
-    when(resourceDao.getLastSnapshotByResourceId(eq(1L))).thenReturn(new SnapshotDto().setPeriod1Mode("previous_analysis"));
+    WsTester.TestRequest request = tester.newGetRequest("api/components", "app").setParam("key", COMPONENT_KEY);
+    request.execute().assertJson(getClass(), "app_with_measures.json");
+  }
+
+  @Test
+  public void app_with_periods() throws Exception {
+    MockUserSession.set().addProjectPermissions(UserRole.CODEVIEWER, PROJECT_KEY).addComponent(COMPONENT_KEY, PROJECT_KEY);
+
+    addProjectSample();
+
+    when(resourceDao.getLastSnapshotByResourceId(eq(1L))).thenReturn(
+      new SnapshotDto().setPeriod1Mode("previous_analysis").setPeriod1Date(DateUtils.parseDate("2014-05-08"))
+    );
     when(periods.label(anyString(), anyString(), any(Date.class))).thenReturn("since previous analysis (May 08 2014)");
 
     WsTester.TestRequest request = tester.newGetRequest("api/components", "app").setParam("key", COMPONENT_KEY);
-    request.execute().assertJson(getClass(), "app.json");
+    request.execute().assertJson(getClass(), "app_with_periods.json");
+  }
+
+  @Test
+  public void app_with_severities() throws Exception {
+    MockUserSession.set().addProjectPermissions(UserRole.CODEVIEWER, PROJECT_KEY).addComponent(COMPONENT_KEY, PROJECT_KEY);
+
+    addProjectSample();
+
+    Multiset<String> severities = HashMultiset.create();
+    severities.add("MAJOR", 5);
+    when(issueService.findSeveritiesByComponent(COMPONENT_KEY)).thenReturn(severities);
+    when(i18n.message(any(Locale.class), eq("severity.MAJOR"), isNull(String.class))).thenReturn("Major");
+
+    WsTester.TestRequest request = tester.newGetRequest("api/components", "app").setParam("key", COMPONENT_KEY);
+    request.execute().assertJson(getClass(), "app_with_severities.json");
+  }
+
+  @Test
+  public void app_with_rules() throws Exception {
+    MockUserSession.set().addProjectPermissions(UserRole.CODEVIEWER, PROJECT_KEY).addComponent(COMPONENT_KEY, PROJECT_KEY);
+
+    addProjectSample();
+    when(issueService.findRulesByComponent(COMPONENT_KEY)).thenReturn(
+      new RulesAggregation().add(new RuleDto().setRuleKey("AvoidCycle").setRepositoryKey("squid").setName("Avoid Cycle"))
+    );
+
+    WsTester.TestRequest request = tester.newGetRequest("api/components", "app").setParam("key", COMPONENT_KEY);
+    request.execute().assertJson(getClass(), "app_with_rules.json");
+  }
+
+  private void addProjectSample() {
+    ComponentDto file = new ComponentDto().setId(10L).setQualifier("FIL").setKey(COMPONENT_KEY).setName("Plugin.java")
+      .setPath("src/main/java/org/sonar/api/Plugin.java").setSubProjectId(5L).setProjectId(1L);
+    when(resourceDao.selectComponentByKey(COMPONENT_KEY)).thenReturn(file);
+    when(resourceDao.findById(5L)).thenReturn(new ComponentDto().setId(5L).setLongName("SonarQube :: Plugin API"));
+    when(resourceDao.findById(1L)).thenReturn(new ComponentDto().setId(1L).setLongName("SonarQube"));
+
   }
 
-  private void addMeasure(String metricKey, Integer value){
+  private void addMeasure(String metricKey, Integer value) {
     when(measureDao.findByComponentKeyAndMetricKey(COMPONENT_KEY, metricKey)).thenReturn(new MeasureDto().setValue(value.doubleValue()));
     when(i18n.formatInteger(any(Locale.class), eq(value.intValue()))).thenReturn(Integer.toString(value));
   }
 
-  private void addMeasure(String metricKey, Double value){
+  private void addMeasure(String metricKey, Double value) {
     when(measureDao.findByComponentKeyAndMetricKey(COMPONENT_KEY, metricKey)).thenReturn(new MeasureDto().setValue(value));
     when(i18n.formatDouble(any(Locale.class), eq(value))).thenReturn(Double.toString(value));
   }
index 6bd1d3e10e72a10bea0968945403b8b8a2d5a964..76f461113f4386f58b6836d1c11cac060d7c3100 100644 (file)
@@ -30,6 +30,7 @@ import org.sonar.core.measure.db.MeasureDao;
 import org.sonar.core.properties.PropertiesDao;
 import org.sonar.core.resource.ResourceDao;
 import org.sonar.core.timemachine.Periods;
+import org.sonar.server.issue.IssueService;
 import org.sonar.server.ws.WsTester;
 
 import static org.fest.assertions.Assertions.assertThat;
@@ -42,7 +43,7 @@ public class ComponentsWsTest {
   @Before
   public void setUp() throws Exception {
     WsTester tester = new WsTester(new ComponentsWs(new ComponentAppAction(mock(ResourceDao.class), mock(MeasureDao.class), mock(PropertiesDao.class),
-      mock(Periods.class), mock(Durations.class), mock(I18n.class))));
+      mock(IssueService.class), mock(Periods.class), mock(Durations.class), mock(I18n.class))));
     controller = tester.controller("api/components");
   }
 
index f62b7272acbd2242af6817b0f5b00ba1a1e102b5..745bc14964994e70c5e5e11d8e4186fbf49ac6af 100644 (file)
@@ -41,6 +41,7 @@ import org.sonar.api.web.UserRole;
 import org.sonar.core.issue.DefaultActionPlan;
 import org.sonar.core.issue.IssueNotifications;
 import org.sonar.core.issue.IssueUpdater;
+import org.sonar.core.issue.db.IssueDao;
 import org.sonar.core.issue.db.IssueStorage;
 import org.sonar.core.issue.workflow.IssueWorkflow;
 import org.sonar.core.issue.workflow.Transition;
@@ -92,6 +93,9 @@ public class IssueServiceTest {
   @Mock
   ResourceDao resourceDao;
 
+  @Mock
+  IssueDao issueDao;
+
   @Mock
   AuthorizationDao authorizationDao;
 
@@ -130,8 +134,8 @@ public class IssueServiceTest {
     when(resource.getKey()).thenReturn("org.sonar.Sample");
     when(project.getKey()).thenReturn("Sample");
 
-    issueService = new IssueService(finder, workflow, issueStorage, issueUpdater, issueNotifications, actionPlanService, ruleFinder, resourceDao, authorizationDao, userFinder,
-      mock(PreviewCache.class));
+    issueService = new IssueService(finder, workflow, issueStorage, issueUpdater, issueNotifications, actionPlanService, ruleFinder, resourceDao, issueDao,
+      authorizationDao, userFinder, mock(PreviewCache.class));
   }
 
   @Test
index e50f1e591bd2b190b5c1467ab2a4ed1a8928ee4a..3b318935cb281356cf38e7ae0f15349e96f79bbf 100644 (file)
@@ -6,19 +6,8 @@
   "subProjectName": "SonarQube :: Plugin API",
   "projectName": "SonarQube",
   "fav": true,
-  "periods": [
-    {"1" : "since previous analysis (May 08 2014)"}
-  ],
-  "measures": {
-    "fNcloc": "200",
-    "fCoverage": "95.4%",
-    "fDuplicationDensity": "7.4%",
-    "fDebt": "3h 2min",
-    "fIssues": "14",
-    "fBlockerIssues": "1",
-    "fCriticalIssues": "2",
-    "fMajorIssues": "5",
-    "fMinorIssues": "4",
-    "fInfoIssues": "2"
-  }
+  "periods": [],
+  "severities": [],
+  "rules": [],
+  "measures": {}
 }
diff --git a/sonar-server/src/test/resources/org/sonar/server/component/ws/ComponentAppActionTest/app_with_measures.json b/sonar-server/src/test/resources/org/sonar/server/component/ws/ComponentAppActionTest/app_with_measures.json
new file mode 100644 (file)
index 0000000..ced3e77
--- /dev/null
@@ -0,0 +1,24 @@
+{
+  "key": "org.codehaus.sonar:sonar-plugin-api:src/main/java/org/sonar/api/Plugin.java",
+  "path": "src/main/java/org/sonar/api/Plugin.java",
+  "name": "Plugin.java",
+  "q": "FIL",
+  "subProjectName": "SonarQube :: Plugin API",
+  "projectName": "SonarQube",
+  "fav": false,
+  "periods": [],
+  "severities": [],
+  "rules": [],
+  "measures": {
+    "fNcloc": "200",
+    "fCoverage": "95.4%",
+    "fDuplicationDensity": "7.4%",
+    "fDebt": "3h 2min",
+    "fIssues": "14",
+    "fBlockerIssues": "1",
+    "fCriticalIssues": "2",
+    "fMajorIssues": "5",
+    "fMinorIssues": "4",
+    "fInfoIssues": "2"
+  }
+}
diff --git a/sonar-server/src/test/resources/org/sonar/server/component/ws/ComponentAppActionTest/app_with_periods.json b/sonar-server/src/test/resources/org/sonar/server/component/ws/ComponentAppActionTest/app_with_periods.json
new file mode 100644 (file)
index 0000000..1cdfe1d
--- /dev/null
@@ -0,0 +1,15 @@
+{
+  "key": "org.codehaus.sonar:sonar-plugin-api:src/main/java/org/sonar/api/Plugin.java",
+  "path": "src/main/java/org/sonar/api/Plugin.java",
+  "name": "Plugin.java",
+  "q": "FIL",
+  "subProjectName": "SonarQube :: Plugin API",
+  "projectName": "SonarQube",
+  "fav": false,
+  "periods": [
+    [1, "since previous analysis (May 08 2014)", "2014-05-08T00:00:00+0200"]
+  ],
+  "severities": [],
+  "rules": [],
+  "measures": {}
+}
diff --git a/sonar-server/src/test/resources/org/sonar/server/component/ws/ComponentAppActionTest/app_with_rules.json b/sonar-server/src/test/resources/org/sonar/server/component/ws/ComponentAppActionTest/app_with_rules.json
new file mode 100644 (file)
index 0000000..9eb08af
--- /dev/null
@@ -0,0 +1,15 @@
+{
+  "key": "org.codehaus.sonar:sonar-plugin-api:src/main/java/org/sonar/api/Plugin.java",
+  "path": "src/main/java/org/sonar/api/Plugin.java",
+  "name": "Plugin.java",
+  "q": "FIL",
+  "subProjectName": "SonarQube :: Plugin API",
+  "projectName": "SonarQube",
+  "fav": false,
+  "periods": [],
+  "severities": [],
+  "rules": [
+    ["squid:AvoidCycle", "Avoid Cycle", 1]
+  ],
+  "measures": {}
+}
diff --git a/sonar-server/src/test/resources/org/sonar/server/component/ws/ComponentAppActionTest/app_with_severities.json b/sonar-server/src/test/resources/org/sonar/server/component/ws/ComponentAppActionTest/app_with_severities.json
new file mode 100644 (file)
index 0000000..ce7fde7
--- /dev/null
@@ -0,0 +1,15 @@
+{
+  "key": "org.codehaus.sonar:sonar-plugin-api:src/main/java/org/sonar/api/Plugin.java",
+  "path": "src/main/java/org/sonar/api/Plugin.java",
+  "name": "Plugin.java",
+  "q": "FIL",
+  "subProjectName": "SonarQube :: Plugin API",
+  "projectName": "SonarQube",
+  "fav": false,
+  "periods": [],
+  "severities": [
+    ["MAJOR", "Major", 5]
+  ],
+  "rules": [],
+  "measures": {}
+}