]> source.dussan.org Git - sonarqube.git/commitdiff
SONAR-4716 Create a component to display remediation cost
authorJulien Lancelot <julien.lancelot@gmail.com>
Wed, 2 Oct 2013 15:34:58 +0000 (17:34 +0200)
committerJulien Lancelot <julien.lancelot@gmail.com>
Wed, 2 Oct 2013 15:35:19 +0000 (17:35 +0200)
plugins/sonar-core-plugin/src/main/java/org/sonar/plugins/core/technicaldebt/TechnicalDebtDecorator.java
sonar-core/src/main/java/org/sonar/core/technicaldebt/RemediationCostTimeUnit.java [new file with mode: 0644]
sonar-core/src/main/java/org/sonar/core/technicaldebt/WorkUnitConverter.java
sonar-core/src/test/java/org/sonar/core/technicaldebt/RemediationCostTimeUnitTest.java [new file with mode: 0644]
sonar-core/src/test/java/org/sonar/core/technicaldebt/WorkUnitConverterTest.java
sonar-server/src/main/java/org/sonar/server/issue/InternalRubyIssueService.java
sonar-server/src/main/java/org/sonar/server/platform/Platform.java
sonar-server/src/main/webapp/WEB-INF/app/views/issue/_issue.html.erb
sonar-server/src/test/java/org/sonar/server/issue/InternalRubyIssueServiceTest.java

index fcc2deb96c6f2a613cdf88f68cc764b5192adbf5..0d13f9f6b24bffa42d01771d188ca09765b1352b 100644 (file)
@@ -113,7 +113,7 @@ public final class TechnicalDebtDecorator implements Decorator {
     return ImmutableList.of(
       PropertyDefinition.builder(WorkUnitConverter.PROPERTY_HOURS_IN_DAY)
         .name("Number of working hours in a day")
-        .defaultValue("" + WorkUnitConverter.DEFAULT_HOURS_IN_DAY)
+        .defaultValue("8")
         .category(CoreProperties.CATEGORY_TECHNICAL_DEBT)
         .build()
     );
diff --git a/sonar-core/src/main/java/org/sonar/core/technicaldebt/RemediationCostTimeUnit.java b/sonar-core/src/main/java/org/sonar/core/technicaldebt/RemediationCostTimeUnit.java
new file mode 100644 (file)
index 0000000..ae7d50e
--- /dev/null
@@ -0,0 +1,92 @@
+/*
+ * SonarQube, open source software quality management tool.
+ * Copyright (C) 2008-2013 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.core.technicaldebt;
+
+import javax.annotation.CheckForNull;
+
+import java.util.concurrent.TimeUnit;
+
+public class RemediationCostTimeUnit {
+
+  private TimeUnitValue days;
+  private TimeUnitValue hours;
+  private TimeUnitValue minutes;
+
+  private static final int ONE_HOUR_IN_MINUTE = 60;
+
+  private RemediationCostTimeUnit(long input, int hoursInDay) {
+    int oneWorkingDay = hoursInDay * ONE_HOUR_IN_MINUTE;
+    if (input >= oneWorkingDay) {
+      long nbDays = input / oneWorkingDay;
+      this.days = new TimeUnitValue(nbDays, TimeUnit.DAYS);
+      input = input - (nbDays * oneWorkingDay);
+    } else {
+      this.days = new TimeUnitValue(0L, TimeUnit.DAYS);
+    }
+
+    if (input >= ONE_HOUR_IN_MINUTE) {
+      long nbHours = input / ONE_HOUR_IN_MINUTE;
+      this.hours = new TimeUnitValue(nbHours, TimeUnit.HOURS);
+      input = input - (nbHours * ONE_HOUR_IN_MINUTE);
+    } else {
+      this.hours = new TimeUnitValue(0L, TimeUnit.HOURS);
+    }
+
+    this.minutes = new TimeUnitValue(input, TimeUnit.MINUTES);
+  }
+
+  public static RemediationCostTimeUnit of(Long time, int hoursInDay) {
+    return new RemediationCostTimeUnit(time, hoursInDay);
+  }
+
+  @CheckForNull
+  public TimeUnitValue days() {
+    return days;
+  }
+
+  @CheckForNull
+  public TimeUnitValue hours() {
+    return hours;
+  }
+
+  @CheckForNull
+  public TimeUnitValue minutes() {
+    return minutes;
+  }
+
+  public static class TimeUnitValue {
+
+    private long value;
+    private TimeUnit unit;
+
+    private TimeUnitValue(long value, TimeUnit unit) {
+      this.value = value;
+      this.unit = unit;
+    }
+
+    public long value() {
+      return value;
+    }
+
+    public TimeUnit unit() {
+      return unit;
+    }
+  }
+}
index 8523b9a3ecb5cbd1ddb1563ece563765c2ac09e0..3c3d26154e12ba8cc8dc89ad3622e6f6d41a9534 100644 (file)
@@ -21,55 +21,51 @@ package org.sonar.core.technicaldebt;
 
 import org.apache.commons.lang.StringUtils;
 import org.sonar.api.BatchComponent;
+import org.sonar.api.ServerComponent;
 import org.sonar.api.config.Settings;
 
-public final class WorkUnitConverter implements BatchComponent {
-
-  public static final int DEFAULT_HOURS_IN_DAY = 8;
+public final class WorkUnitConverter implements BatchComponent, ServerComponent {
 
   public static final String PROPERTY_HOURS_IN_DAY = "sonar.technicalDebt.hoursInDay";
 
-  private int hoursInDay = DEFAULT_HOURS_IN_DAY;
+  private int hoursInDay;
 
   public WorkUnitConverter(Settings settings) {
     this.hoursInDay = settings.getInt(PROPERTY_HOURS_IN_DAY);
   }
 
-  public int getHoursInDay() {
-    return hoursInDay;
-  }
-
   public double toDays(WorkUnit factor) {
-    double result;
     if (StringUtils.equals(WorkUnit.DAYS, factor.getUnit())) {
-      result = factor.getValue();
+      return factor.getValue();
 
     } else if (StringUtils.equals(WorkUnit.HOURS, factor.getUnit())) {
-      result = factor.getValue() / hoursInDay;
+      return factor.getValue() / hoursInDay;
 
     } else if (StringUtils.equals(WorkUnit.MINUTES, factor.getUnit())) {
-      result = factor.getValue() / (hoursInDay * 60.0);
+      return factor.getValue() / (hoursInDay * 60.0);
 
     } else {
       throw new IllegalArgumentException("Unknown remediation factor unit: " + factor.getUnit());
     }
-    return result;
   }
 
   public long toMinutes(WorkUnit factor) {
-    long result;
     if (StringUtils.equals(WorkUnit.DAYS, factor.getUnit())) {
-      result = Double.valueOf(factor.getValue() * hoursInDay * 60d).longValue();
+      return Double.valueOf(factor.getValue() * hoursInDay * 60d).longValue();
 
     } else if (StringUtils.equals(WorkUnit.HOURS, factor.getUnit())) {
-      result = Double.valueOf(factor.getValue() * 60d).longValue();
+      return Double.valueOf(factor.getValue() * 60d).longValue();
 
     } else if (StringUtils.equals(WorkUnit.MINUTES, factor.getUnit())) {
-      result = Double.valueOf(factor.getValue()).longValue();
+      return Double.valueOf(factor.getValue()).longValue();
 
     } else {
       throw new IllegalArgumentException("Unknown remediation factor unit: " + factor.getUnit());
     }
-    return result;
   }
+
+  public RemediationCostTimeUnit toRemediationCostTimeUnit(long minutes){
+    return RemediationCostTimeUnit.of(minutes, hoursInDay);
+  }
+
 }
diff --git a/sonar-core/src/test/java/org/sonar/core/technicaldebt/RemediationCostTimeUnitTest.java b/sonar-core/src/test/java/org/sonar/core/technicaldebt/RemediationCostTimeUnitTest.java
new file mode 100644 (file)
index 0000000..c655b69
--- /dev/null
@@ -0,0 +1,70 @@
+/*
+ * SonarQube, open source software quality management tool.
+ * Copyright (C) 2008-2013 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.core.technicaldebt;
+
+import org.junit.Test;
+
+import java.util.concurrent.TimeUnit;
+
+import static org.fest.assertions.Assertions.assertThat;
+
+public class RemediationCostTimeUnitTest {
+
+  private static final int HOURS_IN_DAY = 8;
+
+  @Test
+  public void convert_simple_values() {
+    checkTimes(RemediationCostTimeUnit.of(15L, HOURS_IN_DAY), 15L, 0L, 0L);
+    checkTimes(RemediationCostTimeUnit.of(120L, HOURS_IN_DAY), 0L, 2L, 0L);
+    checkTimes(RemediationCostTimeUnit.of(480L, HOURS_IN_DAY), 0L, 0L, 1L);
+  }
+
+  @Test
+  public void convert_complex_values() {
+    checkTimes(RemediationCostTimeUnit.of(70L, HOURS_IN_DAY), 10L, 1L, 0L);
+    checkTimes(RemediationCostTimeUnit.of(490L, HOURS_IN_DAY), 10L, 0L, 1L);
+    checkTimes(RemediationCostTimeUnit.of(550L, HOURS_IN_DAY), 10L, 1L, 1L);
+  }
+
+  private void checkTimes(RemediationCostTimeUnit remediationCostTimeUnit, Long expectedMinutes, Long expectedHours, Long expectedDays ) {
+    if (expectedMinutes != null) {
+      checkTime(remediationCostTimeUnit.minutes(), expectedMinutes, TimeUnit.MINUTES);
+    } else {
+      assertThat(remediationCostTimeUnit.minutes().value()).isEqualTo(0L);
+    }
+    if (expectedHours != null) {
+      checkTime(remediationCostTimeUnit.hours(), expectedHours, TimeUnit.HOURS);
+    } else {
+      assertThat(remediationCostTimeUnit.hours().value()).isEqualTo(0L);
+    }
+    if (expectedDays != null) {
+      checkTime(remediationCostTimeUnit.days(), expectedDays, TimeUnit.DAYS);
+    } else {
+      assertThat(remediationCostTimeUnit.days().value()).isEqualTo(0L);
+    }
+  }
+
+  private void checkTime(RemediationCostTimeUnit.TimeUnitValue timeUnitValue, Long expectedValue, TimeUnit expectedUnit) {
+    assertThat(timeUnitValue.value()).isEqualTo(expectedValue);
+    assertThat(timeUnitValue.unit()).isEqualTo(expectedUnit);
+  }
+
+
+}
index be225db71fc0679fef4ef6e30193408589c2c26d..890e84ee3122b68b9a4b336ca13f6d5dbc3c06f1 100644 (file)
@@ -28,7 +28,7 @@ import static org.junit.Assert.assertThat;
 public class WorkUnitConverterTest {
 
   @Test
-  public void concert_to_days() {
+  public void convert_to_days() {
     Settings settings = new Settings();
     settings.setProperty(WorkUnitConverter.PROPERTY_HOURS_IN_DAY, "12");
 
index 84914cf392f4196b1447486976c66cdb40c03ce4..8dde854e09bf9cf9e18a7aa7580f9c69380fe7ec 100644 (file)
@@ -43,6 +43,8 @@ import org.sonar.core.issue.workflow.Transition;
 import org.sonar.core.resource.ResourceDao;
 import org.sonar.core.resource.ResourceDto;
 import org.sonar.core.resource.ResourceQuery;
+import org.sonar.core.technicaldebt.RemediationCostTimeUnit;
+import org.sonar.core.technicaldebt.WorkUnitConverter;
 import org.sonar.server.exceptions.BadRequestException;
 import org.sonar.server.user.UserSession;
 import org.sonar.server.util.RubyUtils;
@@ -81,12 +83,14 @@ public class InternalRubyIssueService implements ServerComponent {
   private final ActionService actionService;
   private final IssueFilterService issueFilterService;
   private final IssueBulkChangeService issueBulkChangeService;
+  private final WorkUnitConverter workUnitConverter;
 
   public InternalRubyIssueService(IssueService issueService,
                                   IssueCommentService commentService,
                                   IssueChangelogService changelogService, ActionPlanService actionPlanService,
                                   IssueStatsFinder issueStatsFinder, ResourceDao resourceDao, ActionService actionService,
-                                  IssueFilterService issueFilterService, IssueBulkChangeService issueBulkChangeService) {
+                                  IssueFilterService issueFilterService, IssueBulkChangeService issueBulkChangeService,
+                                  WorkUnitConverter workUnitConverter) {
     this.issueService = issueService;
     this.commentService = commentService;
     this.changelogService = changelogService;
@@ -96,6 +100,7 @@ public class InternalRubyIssueService implements ServerComponent {
     this.actionService = actionService;
     this.issueFilterService = issueFilterService;
     this.issueBulkChangeService = issueBulkChangeService;
+    this.workUnitConverter = workUnitConverter;
   }
 
   public IssueStatsFinder.IssueStatsResult findIssueAssignees(Map<String, Object> params) {
@@ -338,7 +343,7 @@ public class InternalRubyIssueService implements ServerComponent {
     return result;
   }
 
-  private boolean isActionPlanNameAvailable(@Nullable DefaultActionPlan existingActionPlan, String name, String projectParam){
+  private boolean isActionPlanNameAvailable(@Nullable DefaultActionPlan existingActionPlan, String name, String projectParam) {
     return (existingActionPlan == null || !name.equals(existingActionPlan.name())) && actionPlanService.isNameAlreadyUsedForProject(name, projectParam);
   }
 
@@ -427,7 +432,7 @@ public class InternalRubyIssueService implements ServerComponent {
     }
   }
 
-  public boolean canUserShareIssueFilter(){
+  public boolean canUserShareIssueFilter() {
     return issueFilterService.canShareFilter(UserSession.get());
   }
 
@@ -621,4 +626,14 @@ public class InternalRubyIssueService implements ServerComponent {
     }
   }
 
+  @CheckForNull
+  public RemediationCostTimeUnit remediationCost(Issue issue) {
+    Long remediationCost = issue.remediationCost();
+    if (remediationCost != null) {
+      return workUnitConverter.toRemediationCostTimeUnit(issue.remediationCost());
+    } else {
+      return null;
+    }
+  }
+
 }
index 378d28e6875d42b2a0dac6e3c0bf00de3c2d4d97..9a05474f3def9667953a1791a35584c992ccd8e5 100644 (file)
@@ -58,6 +58,7 @@ import org.sonar.core.qualitymodel.DefaultModelFinder;
 import org.sonar.core.resource.DefaultResourcePermissions;
 import org.sonar.core.rule.DefaultRuleFinder;
 import org.sonar.core.source.HtmlSourceDecorator;
+import org.sonar.core.technicaldebt.WorkUnitConverter;
 import org.sonar.core.test.TestPlanPerspectiveLoader;
 import org.sonar.core.test.TestablePerspectiveLoader;
 import org.sonar.core.timemachine.Periods;
@@ -301,6 +302,7 @@ public final class Platform {
     servicesContainer.addSingleton(TechnicalDebtManager.class);
     servicesContainer.addSingleton(TechnicalDebtModelFinder.class);
     servicesContainer.addSingleton(XMLImporter.class);
+    servicesContainer.addSingleton(WorkUnitConverter.class);
 
     // text
     servicesContainer.addSingleton(MacroInterpreter.class);
index 67097e935b4bcef9821e7ec0af7824dd7a8ce095..440c39499596a8a8a0c65896d00288b989b2f535 100644 (file)
@@ -21,7 +21,8 @@
     <% end %>
     <img src="<%= ApplicationController.root_context -%>/images/sep12.png"/>
     &nbsp;
-    <a href="#" onclick="return toggleIssueChangelog(this)" class="gray issue-changelog-link" id="toggle-issue-changelog"><%= distance_of_time_in_words_to_now(Api::Utils.java_to_ruby_datetime(issue.creationDate())) -%></a>
+    <a href="#" onclick="return toggleIssueChangelog(this)" class="gray issue-changelog-link"
+       id="toggle-issue-changelog"><%= distance_of_time_in_words_to_now(Api::Utils.java_to_ruby_datetime(issue.creationDate())) -%></a>
     &nbsp;
     <% if issue.reporter %>
       <img src="<%= ApplicationController.root_context -%>/images/sep12.png"/>
      <span><%= message('issue.authorLogin') -%> <%= issue.authorLogin -%></span>
       &nbsp;
     <% end %>
+    <% if issue.remediationCost %>
+      <img src="<%= ApplicationController.root_context -%>/images/sep12.png"/>
+      &nbsp;
+      <span><%= issue.remediationCost -%></span>
+      &nbsp;
+    <% end %>
   </div>
 
   <div class="issue-rule rule-desc" style="display: none"></div>
index eb3fc3c561dc04c61b07bac231d1a317f699996b..658b3155ec4bf4d1ec38af171567ab5b4496d9c9 100644 (file)
@@ -64,7 +64,7 @@ public class InternalRubyIssueServiceTest {
     ResourceDto project = new ResourceDto().setKey("org.sonar.Sample");
     when(resourceDao.getResource(any(ResourceQuery.class))).thenReturn(project);
     service = new InternalRubyIssueService(issueService, commentService, changelogService, actionPlanService, issueStatsFinder, resourceDao, actionService,
-      issueFilterService, issueBulkChangeService);
+      issueFilterService, issueBulkChangeService, null);
   }
 
   @Test