From: Julien Lancelot Date: Wed, 2 Oct 2013 15:34:58 +0000 (+0200) Subject: SONAR-4716 Create a component to display remediation cost X-Git-Tag: 4.0~241 X-Git-Url: https://source.dussan.org/?a=commitdiff_plain;h=5fe1b8c293baf38e06f738aa60138b08aec47eb2;p=sonarqube.git SONAR-4716 Create a component to display remediation cost --- diff --git a/plugins/sonar-core-plugin/src/main/java/org/sonar/plugins/core/technicaldebt/TechnicalDebtDecorator.java b/plugins/sonar-core-plugin/src/main/java/org/sonar/plugins/core/technicaldebt/TechnicalDebtDecorator.java index fcc2deb96c6..0d13f9f6b24 100644 --- a/plugins/sonar-core-plugin/src/main/java/org/sonar/plugins/core/technicaldebt/TechnicalDebtDecorator.java +++ b/plugins/sonar-core-plugin/src/main/java/org/sonar/plugins/core/technicaldebt/TechnicalDebtDecorator.java @@ -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 index 00000000000..ae7d50ed9a7 --- /dev/null +++ b/sonar-core/src/main/java/org/sonar/core/technicaldebt/RemediationCostTimeUnit.java @@ -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; + } + } +} diff --git a/sonar-core/src/main/java/org/sonar/core/technicaldebt/WorkUnitConverter.java b/sonar-core/src/main/java/org/sonar/core/technicaldebt/WorkUnitConverter.java index 8523b9a3ecb..3c3d26154e1 100644 --- a/sonar-core/src/main/java/org/sonar/core/technicaldebt/WorkUnitConverter.java +++ b/sonar-core/src/main/java/org/sonar/core/technicaldebt/WorkUnitConverter.java @@ -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 index 00000000000..c655b69449c --- /dev/null +++ b/sonar-core/src/test/java/org/sonar/core/technicaldebt/RemediationCostTimeUnitTest.java @@ -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); + } + + +} diff --git a/sonar-core/src/test/java/org/sonar/core/technicaldebt/WorkUnitConverterTest.java b/sonar-core/src/test/java/org/sonar/core/technicaldebt/WorkUnitConverterTest.java index be225db71fc..890e84ee312 100644 --- a/sonar-core/src/test/java/org/sonar/core/technicaldebt/WorkUnitConverterTest.java +++ b/sonar-core/src/test/java/org/sonar/core/technicaldebt/WorkUnitConverterTest.java @@ -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"); diff --git a/sonar-server/src/main/java/org/sonar/server/issue/InternalRubyIssueService.java b/sonar-server/src/main/java/org/sonar/server/issue/InternalRubyIssueService.java index 84914cf392f..8dde854e09b 100644 --- a/sonar-server/src/main/java/org/sonar/server/issue/InternalRubyIssueService.java +++ b/sonar-server/src/main/java/org/sonar/server/issue/InternalRubyIssueService.java @@ -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 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; + } + } + } diff --git a/sonar-server/src/main/java/org/sonar/server/platform/Platform.java b/sonar-server/src/main/java/org/sonar/server/platform/Platform.java index 378d28e6875..9a05474f3de 100644 --- a/sonar-server/src/main/java/org/sonar/server/platform/Platform.java +++ b/sonar-server/src/main/java/org/sonar/server/platform/Platform.java @@ -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); diff --git a/sonar-server/src/main/webapp/WEB-INF/app/views/issue/_issue.html.erb b/sonar-server/src/main/webapp/WEB-INF/app/views/issue/_issue.html.erb index 67097e935b4..440c3949959 100644 --- a/sonar-server/src/main/webapp/WEB-INF/app/views/issue/_issue.html.erb +++ b/sonar-server/src/main/webapp/WEB-INF/app/views/issue/_issue.html.erb @@ -21,7 +21,8 @@ <% end %>   - <%= distance_of_time_in_words_to_now(Api::Utils.java_to_ruby_datetime(issue.creationDate())) -%> + <%= distance_of_time_in_words_to_now(Api::Utils.java_to_ruby_datetime(issue.creationDate())) -%>   <% if issue.reporter %> @@ -35,6 +36,12 @@ <%= message('issue.authorLogin') -%> <%= issue.authorLogin -%>   <% end %> + <% if issue.remediationCost %> + +   + <%= issue.remediationCost -%> +   + <% end %> diff --git a/sonar-server/src/test/java/org/sonar/server/issue/InternalRubyIssueServiceTest.java b/sonar-server/src/test/java/org/sonar/server/issue/InternalRubyIssueServiceTest.java index eb3fc3c561d..658b3155ec4 100644 --- a/sonar-server/src/test/java/org/sonar/server/issue/InternalRubyIssueServiceTest.java +++ b/sonar-server/src/test/java/org/sonar/server/issue/InternalRubyIssueServiceTest.java @@ -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