]> source.dussan.org Git - sonarqube.git/commitdiff
SONAR-4284 Display a changelog in the Issue detail page
authorSimon Brandhof <simon.brandhof@gmail.com>
Tue, 4 Jun 2013 13:54:15 +0000 (15:54 +0200)
committerSimon Brandhof <simon.brandhof@gmail.com>
Tue, 4 Jun 2013 13:58:18 +0000 (15:58 +0200)
26 files changed:
plugins/sonar-core-plugin/src/main/resources/org/sonar/l10n/core.properties
plugins/sonar-core-plugin/src/test/java/org/sonar/plugins/core/issue/notification/SendIssueNotificationsPostJobTest.java
sonar-core/src/main/java/org/sonar/core/issue/IssueNotifications.java
sonar-core/src/main/java/org/sonar/core/issue/IssueUpdater.java
sonar-core/src/main/java/org/sonar/core/issue/db/IssueChangeDao.java
sonar-core/src/main/java/org/sonar/core/issue/db/IssueStorage.java
sonar-core/src/main/java/org/sonar/core/user/DefaultUserFinder.java
sonar-core/src/main/java/org/sonar/core/user/UserDao.java
sonar-core/src/test/java/org/sonar/core/issue/IssueNotificationsTest.java
sonar-core/src/test/java/org/sonar/core/issue/IssueUpdaterTest.java
sonar-core/src/test/java/org/sonar/core/issue/db/IssueChangeDaoTest.java
sonar-core/src/test/java/org/sonar/core/issue/db/IssueStorageTest.java
sonar-core/src/test/java/org/sonar/core/user/UserDaoTest.java
sonar-plugin-api/src/main/java/org/sonar/api/issue/internal/DefaultIssue.java
sonar-plugin-api/src/main/java/org/sonar/api/issue/internal/FieldDiffs.java
sonar-plugin-api/src/main/java/org/sonar/api/user/UserFinder.java
sonar-server/src/main/java/org/sonar/server/issue/InternalRubyIssueService.java
sonar-server/src/main/java/org/sonar/server/issue/IssueChangeService.java [deleted file]
sonar-server/src/main/java/org/sonar/server/issue/IssueChangelog.java [new file with mode: 0644]
sonar-server/src/main/java/org/sonar/server/issue/IssueChangelogService.java [new file with mode: 0644]
sonar-server/src/main/java/org/sonar/server/issue/IssueCommentService.java [new file with mode: 0644]
sonar-server/src/main/java/org/sonar/server/platform/Platform.java
sonar-server/src/main/webapp/WEB-INF/app/controllers/issue_controller.rb
sonar-server/src/main/webapp/WEB-INF/app/views/issue/_changelog.html.erb
sonar-server/src/test/java/org/sonar/server/issue/InternalRubyIssueServiceTest.java
sonar-server/src/test/java/org/sonar/server/issue/IssueChangelogServiceTest.java [new file with mode: 0644]

index e3f1199d772834c6db409d913d4921445b71f46a..4355c95aad8c219d3fbe2fc69a635b6b672d76a7 100644 (file)
@@ -106,6 +106,7 @@ refresh=Refresh
 remove=Remove
 rename=Rename
 reset_verb=Reset
+resolution=Resolution
 result=Result
 results=Results
 x_results={0} results
@@ -501,7 +502,6 @@ issue.planned_for=Planned for
 issue.manual.missing_rule=Missing rule
 issue.manual.no_rules=No rules.
 issue.reported_by=Reported by
-issue.component_deleted=Removed
 
 
 #------------------------------------------------------------------------------
@@ -989,7 +989,7 @@ widget.my_reviews.no_issue=No issue.
 widget.my_reviews.property.numberOfLines.name=Number of lines
 widget.my_reviews.property.numberOfLines.desc=Maximum number of issues displayed at the same time.
 
-widget.false_positive_reviews.name=False Positives
+widget.false_positive_reviews.name=False Positives Issues
 widget.false_positive_reviews.description=Shows all the false positives found on the project.
 widget.false_positive_reviews.property.numberOfLines.name=Number of lines
 widget.false_positive_reviews.property.numberOfLines.desc=Maximum number of issues displayed at the same time.
@@ -1004,7 +1004,7 @@ widget.unresolved_issues_statuses.description=Displays the number of unresolved
 widget.action_plans.name=Action Plans
 widget.action_plans.description=Shows all the open action plans of the project.
 widget.action_plans.property.showResolvedIssues.name=Show Resolved Issues
-widget.action_plans.title=Open Action Plans
+widget.action_plans.title=Open action plans
 widget.action_plans.no_action_plan=No action plan
 widget.action_plans.x_unresolved_issues={0} unresolved issues
 
@@ -2220,16 +2220,6 @@ global_role.profileadmin=Quality Profile Administrators
 global_role.profileadmin.desc=Ability to perform any action on the quality profiles.
 
 
-#------------------------------------------------------------------------------
-#
-# PROJECTS ROLES
-#
-#------------------------------------------------------------------------------
-projects_role.role=Role Membership For New {0}
-projects_role.users=Users
-projects_role.groups=Groups
-
-
 #------------------------------------------------------------------------------
 #
 # ERRORS HANDLING
index 055af97946eb3e5ac9f48e630127e6056b28a40d..fd9af8ea8ecbb5a1f374c6f811af609c4eb515b1 100644 (file)
@@ -91,7 +91,7 @@ public class SendIssueNotificationsPostJobTest {
     Rule rule = new Rule("squid", "AvoidCycles");
     DefaultIssue issue = new DefaultIssue()
       .setChanged(true)
-      .setFieldDiff(mock(IssueChangeContext.class), "severity", "MINOR", "BLOCKER")
+      .setFieldChange(mock(IssueChangeContext.class), "severity", "MINOR", "BLOCKER")
       .setRuleKey(ruleKey);
     when(issueCache.all()).thenReturn(Arrays.asList(issue));
     when(ruleFinder.findByKey(ruleKey)).thenReturn(rule);
@@ -110,7 +110,7 @@ public class SendIssueNotificationsPostJobTest {
     RuleKey ruleKey = RuleKey.of("squid", "AvoidCycles");
     DefaultIssue issue = new DefaultIssue()
       .setChanged(true)
-      .setFieldDiff(changeContext, "severity", "MINOR", "BLOCKER")
+      .setFieldChange(changeContext, "severity", "MINOR", "BLOCKER")
       .setRuleKey(ruleKey);
     when(issueCache.all()).thenReturn(Arrays.asList(issue));
     when(ruleFinder.findByKey(ruleKey)).thenReturn(null);
index f10e4da4ff6ab65aa9d3124275623a8b98c57e13..3d2013e6a410a7191e85d48dce397c8d2a135ba5 100644 (file)
@@ -87,7 +87,7 @@ public class IssueNotifications implements BatchComponent, ServerComponent {
 
   @CheckForNull
   private Notification createChangeNotification(DefaultIssue issue, IssueChangeContext context, Rule rule, Component project, @Nullable Component component, @Nullable String comment) {
-    if (comment == null && (issue.diffs() == null || issue.diffs().diffs().isEmpty())) {
+    if (comment == null && (issue.currentChange() == null || issue.currentChange().diffs().isEmpty())) {
       return null;
     }
     Notification notification = newNotification(project, "issue-changes");
@@ -105,7 +105,7 @@ public class IssueNotifications implements BatchComponent, ServerComponent {
       notification.setFieldValue("comment", comment);
     }
 
-    FieldDiffs diffs = issue.diffs();
+    FieldDiffs diffs = issue.currentChange();
     if (diffs != null) {
       for (Map.Entry<String, FieldDiffs.Diff> entry : diffs.diffs().entrySet()) {
         String type = entry.getKey();
index 66bda0c6e28e25676c4fe276d55a18a721238b9a..a91d401e0df2166b9365f749469054ffbe8728ce 100644 (file)
@@ -41,7 +41,7 @@ public class IssueUpdater implements BatchComponent, ServerComponent {
       throw new IllegalStateException("Severity can't be changed");
     }
     if (!Objects.equal(severity, issue.severity())) {
-      issue.setFieldDiff(context, "severity", issue.severity(), severity);
+      issue.setFieldChange(context, "severity", issue.severity(), severity);
       issue.setSeverity(severity);
       issue.setUpdateDate(context.date());
       issue.setChanged(true);
@@ -58,7 +58,7 @@ public class IssueUpdater implements BatchComponent, ServerComponent {
 
   public boolean setManualSeverity(DefaultIssue issue, String severity, IssueChangeContext context) {
     if (!issue.manualSeverity() || !Objects.equal(severity, issue.severity())) {
-      issue.setFieldDiff(context, "severity", issue.severity(), severity);
+      issue.setFieldChange(context, "severity", issue.severity(), severity);
       issue.setSeverity(severity);
       issue.setManualSeverity(true);
       issue.setUpdateDate(context.date());
@@ -71,7 +71,7 @@ public class IssueUpdater implements BatchComponent, ServerComponent {
   public boolean assign(DefaultIssue issue, @Nullable String assignee, IssueChangeContext context) {
     String sanitizedAssignee = StringUtils.defaultIfBlank(assignee, null);
     if (!Objects.equal(sanitizedAssignee, issue.assignee())) {
-      issue.setFieldDiff(context, "assignee", issue.assignee(), sanitizedAssignee);
+      issue.setFieldChange(context, "assignee", issue.assignee(), sanitizedAssignee);
       issue.setAssignee(sanitizedAssignee);
       issue.setUpdateDate(context.date());
       issue.setChanged(true);
@@ -97,7 +97,7 @@ public class IssueUpdater implements BatchComponent, ServerComponent {
 
   public boolean setResolution(DefaultIssue issue, @Nullable String resolution, IssueChangeContext context) {
     if (!Objects.equal(resolution, issue.resolution())) {
-      issue.setFieldDiff(context, "resolution", issue.resolution(), resolution);
+      issue.setFieldChange(context, "resolution", issue.resolution(), resolution);
       issue.setResolution(resolution);
       issue.setUpdateDate(context.date());
       issue.setChanged(true);
@@ -108,7 +108,7 @@ public class IssueUpdater implements BatchComponent, ServerComponent {
 
   public boolean setStatus(DefaultIssue issue, String status, IssueChangeContext context) {
     if (!Objects.equal(status, issue.status())) {
-      issue.setFieldDiff(context, "status", issue.status(), status);
+      issue.setFieldChange(context, "status", issue.status(), status);
       issue.setStatus(status);
       issue.setUpdateDate(context.date());
       issue.setChanged(true);
@@ -119,7 +119,7 @@ public class IssueUpdater implements BatchComponent, ServerComponent {
 
   public boolean setAuthorLogin(DefaultIssue issue, @Nullable String authorLogin, IssueChangeContext context) {
     if (!Objects.equal(authorLogin, issue.authorLogin())) {
-      issue.setFieldDiff(context, "author", issue.authorLogin(), authorLogin);
+      issue.setFieldChange(context, "author", issue.authorLogin(), authorLogin);
       issue.setAuthorLogin(authorLogin);
       issue.setUpdateDate(context.date());
       issue.setChanged(true);
@@ -177,7 +177,7 @@ public class IssueUpdater implements BatchComponent, ServerComponent {
   public boolean setAttribute(DefaultIssue issue, String key, @Nullable String value, IssueChangeContext context) {
     String oldValue = issue.attribute(key);
     if (!Objects.equal(oldValue, value)) {
-      issue.setFieldDiff(context, key, oldValue, value);
+      issue.setFieldChange(context, key, oldValue, value);
       issue.setAttribute(key, value);
       issue.setUpdateDate(context.date());
       issue.setChanged(true);
@@ -189,7 +189,7 @@ public class IssueUpdater implements BatchComponent, ServerComponent {
   public boolean plan(DefaultIssue issue, @Nullable String actionPlanKey, IssueChangeContext context) {
     String sanitizedActionPlanKey = StringUtils.defaultIfBlank(actionPlanKey, null);
     if (!Objects.equal(sanitizedActionPlanKey, issue.actionPlanKey())) {
-      issue.setFieldDiff(context, "actionPlanKey", issue.actionPlanKey(), sanitizedActionPlanKey);
+      issue.setFieldChange(context, "actionPlanKey", issue.actionPlanKey(), sanitizedActionPlanKey);
       issue.setActionPlanKey(sanitizedActionPlanKey);
       issue.setUpdateDate(context.date());
       issue.setChanged(true);
index c06f5d61b8e8199ee68b1ea25768cb64b7695d79..3061586f253a4bd1fd3423f86c2fc7f8938eb253 100644 (file)
@@ -26,11 +26,13 @@ import org.apache.ibatis.session.SqlSession;
 import org.sonar.api.BatchComponent;
 import org.sonar.api.ServerComponent;
 import org.sonar.api.issue.internal.DefaultIssueComment;
+import org.sonar.api.issue.internal.FieldDiffs;
 import org.sonar.core.persistence.MyBatis;
 
 import javax.annotation.CheckForNull;
-
+import java.util.Arrays;
 import java.util.Collection;
+import java.util.Collections;
 import java.util.List;
 
 /**
@@ -45,15 +47,21 @@ public class IssueChangeDao implements BatchComponent, ServerComponent {
   }
 
   public List<DefaultIssueComment> selectCommentsByIssues(SqlSession session, Collection<String> issueKeys) {
-    return selectByIssuesAndType(session, issueKeys, IssueChangeDto.TYPE_COMMENT);
+    List<DefaultIssueComment> comments = Lists.newArrayList();
+    for (IssueChangeDto dto : selectByIssuesAndType(session, issueKeys, IssueChangeDto.TYPE_COMMENT)) {
+      comments.add(dto.toComment());
+    }
+    return comments;
   }
 
-  public List<IssueChangeDto> selectIssueChangelog(String issueKey) {
+  public List<FieldDiffs> selectChangelogByIssue(String issueKey) {
     SqlSession session = mybatis.openSession();
     try {
-      IssueChangeMapper mapper = session.getMapper(IssueChangeMapper.class);
-      return mapper.selectByIssue(issueKey);
-
+      List<FieldDiffs> result = Lists.newArrayList();
+      for (IssueChangeDto dto : selectByIssuesAndType(session, Arrays.asList(issueKey), IssueChangeDto.TYPE_FIELD_CHANGE)) {
+        result.add(dto.toFieldDiffs());
+      }
+      return result;
     } finally {
       MyBatis.closeQuietly(session);
     }
@@ -72,17 +80,13 @@ public class IssueChangeDao implements BatchComponent, ServerComponent {
     }
   }
 
-  List<DefaultIssueComment> selectByIssuesAndType(SqlSession session, Collection<String> issueKeys, String changeType) {
+  List<IssueChangeDto> selectByIssuesAndType(SqlSession session, Collection<String> issueKeys, String changeType) {
     Preconditions.checkArgument(issueKeys.size() < 1000, "Number of issue keys is greater than or equal 1000");
-    List<DefaultIssueComment> result = Lists.newArrayList();
-    if (!issueKeys.isEmpty()) {
-      IssueChangeMapper mapper = session.getMapper(IssueChangeMapper.class);
-      List<IssueChangeDto> dtos = mapper.selectByIssuesAndType(issueKeys, changeType);
-      for (IssueChangeDto dto : dtos) {
-        result.add(dto.toComment());
-      }
+    if (issueKeys.isEmpty()) {
+      return Collections.emptyList();
     }
-    return result;
+    IssueChangeMapper mapper = session.getMapper(IssueChangeMapper.class);
+    return mapper.selectByIssuesAndType(issueKeys, changeType);
   }
 
   public boolean delete(String key) {
index eddc19c23f7798c9e5acd30ad449c2427755c2e6..b8287a836559b9dac758d974ec4da139eec10cb1 100644 (file)
@@ -88,7 +88,7 @@ public abstract class IssueStorage {
         mapper.insert(changeDto);
       }
     }
-    FieldDiffs diffs = issue.diffs();
+    FieldDiffs diffs = issue.currentChange();
     if (diffs != null) {
       IssueChangeDto changeDto = IssueChangeDto.of(issue.key(), diffs);
       mapper.insert(changeDto);
index 9773d17f26ea534913fb4ace5ff7f3910babb4ff..2ddfb1a76c929376ceae70055b1ac2054623a4da 100644 (file)
@@ -25,6 +25,7 @@ import org.sonar.api.user.UserFinder;
 import org.sonar.api.user.UserQuery;
 
 import javax.annotation.CheckForNull;
+import java.util.Collection;
 import java.util.List;
 
 /**
@@ -57,7 +58,7 @@ public class DefaultUserFinder implements UserFinder {
     return toUsers(dtos);
   }
 
-  private List<User> toUsers(List<UserDto> dtos) {
+  private List<User> toUsers(Collection<UserDto> dtos) {
     List<User> users = Lists.newArrayList();
     for (UserDto dto : dtos) {
       users.add(dto.toUser());
index 01bc6aae388613097601485612906a89f42ab591..32b3978e6b069fa72ec8995ad11821dfc4d4b1d5 100644 (file)
@@ -25,6 +25,7 @@ import org.sonar.api.user.UserQuery;
 import org.sonar.core.persistence.MyBatis;
 
 import javax.annotation.CheckForNull;
+import java.util.Collection;
 import java.util.List;
 
 /**
index eae906010a993d709bb52e0d9d79a36611842351..7f14933a9851338aebcc22a6b810bc647e26cd4a 100644 (file)
@@ -74,8 +74,8 @@ public class IssueNotificationsTest {
       .setMessage("the message")
       .setKey("ABCDE")
       .setAssignee("freddy")
-      .setFieldDiff(context, "resolution", null, "FIXED")
-      .setFieldDiff(context, "status", "OPEN", "RESOLVED")
+      .setFieldChange(context, "resolution", null, "FIXED")
+      .setFieldChange(context, "status", "OPEN", "RESOLVED")
       .setComponentKey("struts:Action")
       .setProjectKey("struts");
     DefaultIssueQueryResult queryResult = new DefaultIssueQueryResult(Arrays.<Issue>asList(issue));
index 6a87cdc8674395c4752df571b9bd55f1d0cfb288..ade4e76cbd2225b4718732001e1bf13e456e73f7 100644 (file)
@@ -39,7 +39,7 @@ public class IssueUpdaterTest {
     boolean updated = updater.assign(issue, "emmerik", context);
     assertThat(updated).isTrue();
     assertThat(issue.assignee()).isEqualTo("emmerik");
-    FieldDiffs.Diff diff = issue.diffs().get("assignee");
+    FieldDiffs.Diff diff = issue.currentChange().get("assignee");
     assertThat(diff.oldValue()).isNull();
     assertThat(diff.newValue()).isEqualTo("emmerik");
   }
@@ -50,7 +50,7 @@ public class IssueUpdaterTest {
     boolean updated = updater.assign(issue, null, context);
     assertThat(updated).isTrue();
     assertThat(issue.assignee()).isNull();
-    FieldDiffs.Diff diff = issue.diffs().get("assignee");
+    FieldDiffs.Diff diff = issue.currentChange().get("assignee");
     assertThat(diff.oldValue()).isEqualTo("morgan");
     assertThat(diff.newValue()).isNull();
   }
@@ -61,7 +61,7 @@ public class IssueUpdaterTest {
     boolean updated = updater.assign(issue, "emmerik", context);
     assertThat(updated).isTrue();
     assertThat(issue.assignee()).isEqualTo("emmerik");
-    FieldDiffs.Diff diff = issue.diffs().get("assignee");
+    FieldDiffs.Diff diff = issue.currentChange().get("assignee");
     assertThat(diff.oldValue()).isEqualTo("morgan");
     assertThat(diff.newValue()).isEqualTo("emmerik");
   }
@@ -71,7 +71,7 @@ public class IssueUpdaterTest {
     issue.setAssignee("morgan");
     boolean updated = updater.assign(issue, "morgan", context);
     assertThat(updated).isFalse();
-    assertThat(issue.diffs()).isNull();
+    assertThat(issue.currentChange()).isNull();
   }
 
 
@@ -82,7 +82,7 @@ public class IssueUpdaterTest {
     assertThat(issue.severity()).isEqualTo("BLOCKER");
     assertThat(issue.manualSeverity()).isFalse();
 
-    FieldDiffs.Diff diff = issue.diffs().get("severity");
+    FieldDiffs.Diff diff = issue.currentChange().get("severity");
     assertThat(diff.oldValue()).isNull();
     assertThat(diff.newValue()).isEqualTo("BLOCKER");
   }
@@ -94,7 +94,7 @@ public class IssueUpdaterTest {
     assertThat(updated).isTrue();
     assertThat(issue.severity()).isEqualTo("BLOCKER");
 
-    FieldDiffs.Diff diff = issue.diffs().get("severity");
+    FieldDiffs.Diff diff = issue.currentChange().get("severity");
     assertThat(diff.oldValue()).isEqualTo("INFO");
     assertThat(diff.newValue()).isEqualTo("BLOCKER");
   }
@@ -106,7 +106,7 @@ public class IssueUpdaterTest {
 
     assertThat(updated).isTrue();
     assertThat(issue.severity()).isEqualTo("MINOR");
-    FieldDiffs.Diff diff = issue.diffs().get("severity");
+    FieldDiffs.Diff diff = issue.currentChange().get("severity");
     assertThat(diff.oldValue()).isEqualTo("BLOCKER");
     assertThat(diff.newValue()).isEqualTo("MINOR");
   }
@@ -116,7 +116,7 @@ public class IssueUpdaterTest {
     issue.setSeverity("MINOR");
     boolean updated = updater.setSeverity(issue, "MINOR", context);
     assertThat(updated).isFalse();
-    assertThat(issue.diffs()).isNull();
+    assertThat(issue.currentChange()).isNull();
   }
 
   @Test
@@ -137,7 +137,7 @@ public class IssueUpdaterTest {
     assertThat(updated).isTrue();
     assertThat(issue.severity()).isEqualTo("MINOR");
     assertThat(issue.manualSeverity()).isTrue();
-    FieldDiffs.Diff diff = issue.diffs().get("severity");
+    FieldDiffs.Diff diff = issue.currentChange().get("severity");
     assertThat(diff.oldValue()).isEqualTo("BLOCKER");
     assertThat(diff.newValue()).isEqualTo("MINOR");
   }
@@ -147,7 +147,7 @@ public class IssueUpdaterTest {
     issue.setSeverity("MINOR").setManualSeverity(true);
     boolean updated = updater.setManualSeverity(issue, "MINOR", context);
     assertThat(updated).isFalse();
-    assertThat(issue.diffs()).isNull();
+    assertThat(issue.currentChange()).isNull();
   }
 
   @Test
@@ -157,7 +157,7 @@ public class IssueUpdaterTest {
     assertThat(issue.line()).isEqualTo(123);
 
     // do not save change
-    assertThat(issue.diffs()).isNull();
+    assertThat(issue.currentChange()).isNull();
   }
 
   @Test
@@ -168,7 +168,7 @@ public class IssueUpdaterTest {
     assertThat(issue.line()).isEqualTo(42);
 
     // do not save change
-    assertThat(issue.diffs()).isNull();
+    assertThat(issue.currentChange()).isNull();
   }
 
   @Test
@@ -177,7 +177,7 @@ public class IssueUpdaterTest {
     boolean updated = updater.setLine(issue, 123);
     assertThat(updated).isFalse();
     assertThat(issue.line()).isEqualTo(123);
-    assertThat(issue.diffs()).isNull();
+    assertThat(issue.currentChange()).isNull();
   }
 
   @Test
@@ -186,7 +186,7 @@ public class IssueUpdaterTest {
     assertThat(updated).isTrue();
     assertThat(issue.resolution()).isEqualTo("OPEN");
 
-    FieldDiffs.Diff diff = issue.diffs().get("resolution");
+    FieldDiffs.Diff diff = issue.currentChange().get("resolution");
     assertThat(diff.oldValue()).isNull();
     assertThat(diff.newValue()).isEqualTo("OPEN");
   }
@@ -197,7 +197,7 @@ public class IssueUpdaterTest {
     boolean updated = updater.setResolution(issue, "FIXED", context);
     assertThat(updated).isFalse();
     assertThat(issue.resolution()).isEqualTo("FIXED");
-    assertThat(issue.diffs()).isNull();
+    assertThat(issue.currentChange()).isNull();
   }
 
   @Test
@@ -206,7 +206,7 @@ public class IssueUpdaterTest {
     assertThat(updated).isTrue();
     assertThat(issue.status()).isEqualTo("OPEN");
 
-    FieldDiffs.Diff diff = issue.diffs().get("status");
+    FieldDiffs.Diff diff = issue.currentChange().get("status");
     assertThat(diff.oldValue()).isNull();
     assertThat(diff.newValue()).isEqualTo("OPEN");
   }
@@ -217,7 +217,7 @@ public class IssueUpdaterTest {
     boolean updated = updater.setStatus(issue, "CLOSED", context);
     assertThat(updated).isFalse();
     assertThat(issue.status()).isEqualTo("CLOSED");
-    assertThat(issue.diffs()).isNull();
+    assertThat(issue.currentChange()).isNull();
   }
 
   @Test
@@ -225,9 +225,9 @@ public class IssueUpdaterTest {
     boolean updated = updater.setAttribute(issue, "JIRA", "FOO-123", context);
     assertThat(updated).isTrue();
     assertThat(issue.attribute("JIRA")).isEqualTo("FOO-123");
-    assertThat(issue.diffs().diffs()).hasSize(1);
-    assertThat(issue.diffs().get("JIRA").oldValue()).isNull();
-    assertThat(issue.diffs().get("JIRA").newValue()).isEqualTo("FOO-123");
+    assertThat(issue.currentChange().diffs()).hasSize(1);
+    assertThat(issue.currentChange().get("JIRA").oldValue()).isNull();
+    assertThat(issue.currentChange().get("JIRA").newValue()).isEqualTo("FOO-123");
   }
 
   @Test
@@ -236,9 +236,9 @@ public class IssueUpdaterTest {
     boolean updated = updater.setAttribute(issue, "JIRA", null, context);
     assertThat(updated).isTrue();
     assertThat(issue.attribute("JIRA")).isNull();
-    assertThat(issue.diffs().diffs()).hasSize(1);
-    assertThat(issue.diffs().get("JIRA").oldValue()).isEqualTo("FOO-123");
-    assertThat(issue.diffs().get("JIRA").newValue()).isNull();
+    assertThat(issue.currentChange().diffs()).hasSize(1);
+    assertThat(issue.currentChange().get("JIRA").oldValue()).isEqualTo("FOO-123");
+    assertThat(issue.currentChange().get("JIRA").newValue()).isNull();
   }
 
   @Test
@@ -254,7 +254,7 @@ public class IssueUpdaterTest {
     assertThat(updated).isTrue();
     assertThat(issue.actionPlanKey()).isEqualTo("ABCD");
 
-    FieldDiffs.Diff diff = issue.diffs().get("actionPlanKey");
+    FieldDiffs.Diff diff = issue.currentChange().get("actionPlanKey");
     assertThat(diff.oldValue()).isNull();
     assertThat(diff.newValue()).isEqualTo("ABCD");
   }
@@ -284,7 +284,7 @@ public class IssueUpdaterTest {
     assertThat(issue.effortToFix()).isEqualTo(3.14);
 
     // do not save change
-    assertThat(issue.diffs()).isNull();
+    assertThat(issue.currentChange()).isNull();
   }
 
   @Test
@@ -303,7 +303,7 @@ public class IssueUpdaterTest {
     assertThat(issue.message()).isEqualTo("new message");
 
     // do not save change
-    assertThat(issue.diffs()).isNull();
+    assertThat(issue.currentChange()).isNull();
   }
 
   @Test
@@ -312,7 +312,7 @@ public class IssueUpdaterTest {
     assertThat(updated).isTrue();
     assertThat(issue.authorLogin()).isEqualTo("eric");
 
-    FieldDiffs.Diff diff = issue.diffs().get("author");
+    FieldDiffs.Diff diff = issue.currentChange().get("author");
     assertThat(diff.oldValue()).isNull();
     assertThat(diff.newValue()).isEqualTo("eric");
   }
index 21a3f64bd34a462042fe5291325761b958659637..9c3bf875728c4dfe207449826881e325ed82b6f3 100644 (file)
@@ -23,6 +23,7 @@ import org.apache.ibatis.session.SqlSession;
 import org.junit.Before;
 import org.junit.Test;
 import org.sonar.api.issue.internal.DefaultIssueComment;
+import org.sonar.api.issue.internal.FieldDiffs;
 import org.sonar.api.utils.DateUtils;
 import org.sonar.core.persistence.AbstractDaoTestCase;
 
@@ -63,16 +64,28 @@ public class IssueChangeDaoTest extends AbstractDaoTestCase {
   }
 
   @Test
-  public void selectIssueChangelog() {
+  public void selectCommentByKey() {
     setupData("shared");
 
-    List<IssueChangeDto> changes = dao.selectIssueChangelog("1000");
-    assertThat(changes).hasSize(3);
+    DefaultIssueComment comment = dao.selectCommentByKey("FGHIJ");
+    assertThat(comment).isNotNull();
+    assertThat(comment.key()).isEqualTo("FGHIJ");
+    assertThat(comment.key()).isEqualTo("FGHIJ");
+    assertThat(comment.userLogin()).isEqualTo("arthur");
 
-    // chronological order
-    assertThat(changes.get(0).getId()).isEqualTo(100);
-    assertThat(changes.get(1).getId()).isEqualTo(101);
-    assertThat(changes.get(2).getId()).isEqualTo(102);
+    assertThat(dao.selectCommentByKey("UNKNOWN")).isNull();
+  }
+
+
+  @Test
+  public void selectIssueChangelog() {
+    setupData("shared");
+
+    List<FieldDiffs> changelog = dao.selectChangelogByIssue("1000");
+    assertThat(changelog).hasSize(1);
+    assertThat(changelog.get(0).diffs()).hasSize(1);
+    assertThat(changelog.get(0).diffs().get("severity").newValue()).isEqualTo("BLOCKER");
+    assertThat(changelog.get(0).diffs().get("severity").oldValue()).isEqualTo("MAJOR");
   }
 
   @Test
index b65d623e911b2ed170d9dcd4fee2f921b64ab6d1..b8744185375802f6867355091fc83dc621f119ca 100644 (file)
@@ -89,7 +89,7 @@ public class IssueStorageTest extends AbstractDaoTestCase {
       .setChecksum("FFFFF")
       .setAuthorLogin("simon")
       .setAssignee("loic")
-      .setFieldDiff(context, "severity", "INFO", "BLOCKER")
+      .setFieldChange(context, "severity", "INFO", "BLOCKER")
       .setReporter("emmerik")
       .setResolution("FIXED")
       .setStatus("RESOLVED")
index 186b20eaf56c0e28c2320a619bd640c34c9a433a..3df4e1d875e84810e1ec2575fbb39dad383abd14 100644 (file)
@@ -25,6 +25,7 @@ import org.sonar.api.user.UserQuery;
 import org.sonar.core.persistence.AbstractDaoTestCase;
 
 import java.util.Arrays;
+import java.util.Collection;
 import java.util.Collections;
 import java.util.List;
 
@@ -60,7 +61,7 @@ public class UserDaoTest extends AbstractDaoTestCase {
   public void selectUsersByLogins() throws Exception {
     setupData("selectUsersByLogins");
 
-    List<UserDto> users = dao.selectUsersByLogins(Arrays.asList("marius", "inactive_user", "other"));
+    Collection<UserDto> users = dao.selectUsersByLogins(Arrays.asList("marius", "inactive_user", "other"));
     assertThat(users).hasSize(2);
     assertThat(users).onProperty("login").containsOnly("marius", "inactive_user");
   }
@@ -68,7 +69,7 @@ public class UserDaoTest extends AbstractDaoTestCase {
   @Test
   public void selectUsersByLogins_empty_logins() throws Exception {
     // no need to access db
-    List<UserDto> users = dao.selectUsersByLogins(Collections.<String>emptyList());
+    Collection<UserDto> users = dao.selectUsersByLogins(Collections.<String>emptyList());
     assertThat(users).isEmpty();
   }
 
index 11a9e67dcb747ecdae1d7e2b2cf14d343befe4a2..e594b75414539027cb3714855ead55417a237cdd 100644 (file)
@@ -63,16 +63,19 @@ public class DefaultIssue implements Issue {
   private String checksum;
   private Map<String, String> attributes = null;
   private String authorLogin = null;
-  private FieldDiffs diffs = null;
   private String actionPlanKey;
   private List<IssueComment> comments = null;
 
-  // functional dates
+  // FUNCTIONAL DATES
   private Date creationDate;
   private Date updateDate;
   private Date closeDate;
 
-  // The following states are used only during scan.
+
+  // FOLLOWING FIELDS ARE AVAILABLE ONLY DURING SCAN
+
+  // Current changes
+  private FieldDiffs currentChange = null;
 
   // true if the the issue did not exist in the previous scan.
   private boolean isNew = true;
@@ -348,20 +351,20 @@ public class DefaultIssue implements Issue {
     return this;
   }
 
-  public DefaultIssue setFieldDiff(IssueChangeContext context, String field, @Nullable Serializable oldValue, @Nullable Serializable newValue) {
+  public DefaultIssue setFieldChange(IssueChangeContext context, String field, @Nullable Serializable oldValue, @Nullable Serializable newValue) {
     if (!Objects.equal(oldValue, newValue)) {
-      if (diffs == null) {
-        diffs = new FieldDiffs();
-        diffs.setUserLogin(context.login());
+      if (currentChange == null) {
+        currentChange = new FieldDiffs();
+        currentChange.setUserLogin(context.login());
       }
-      diffs.setDiff(field, oldValue, newValue);
+      currentChange.setDiff(field, oldValue, newValue);
     }
     return this;
   }
 
   @CheckForNull
-  public FieldDiffs diffs() {
-    return diffs;
+  public FieldDiffs currentChange() {
+    return currentChange;
   }
 
   public DefaultIssue addComment(DefaultIssueComment comment) {
index 5479bfb4392cd4b6d65ae58de8ab30e6baeeb3ae..b0d44ee6bf94daa38bb1328959704667c38ecd08 100644 (file)
@@ -25,7 +25,6 @@ import com.google.common.collect.Maps;
 
 import javax.annotation.CheckForNull;
 import javax.annotation.Nullable;
-
 import java.io.Serializable;
 import java.util.Date;
 import java.util.Map;
@@ -79,7 +78,7 @@ public class FieldDiffs implements Serializable {
 
 
   @SuppressWarnings("unchecked")
-  public void setDiff(String field, @Nullable Serializable oldValue, @Nullable Serializable newValue) {
+  public FieldDiffs setDiff(String field, @Nullable Serializable oldValue, @Nullable Serializable newValue) {
     Diff diff = diffs.get(field);
     if (diff == null) {
       diff = new Diff(oldValue, newValue);
@@ -87,6 +86,7 @@ public class FieldDiffs implements Serializable {
     } else {
       diff.setNewValue(newValue);
     }
+    return this;
   }
 
   @Override
index 66234bb177d66e8213e67a0903149c29ae1965ca..6bf2c76017ce5fae319cc4934da94314a7f9edeb 100644 (file)
@@ -22,6 +22,7 @@ package org.sonar.api.user;
 import org.sonar.api.ServerComponent;
 
 import javax.annotation.CheckForNull;
+import java.util.Collection;
 import java.util.List;
 
 /**
index f8ad8068b629050391653f3f6d917efd57e677dc..87533a774abb14fee5153ad40aed776da3571d16 100644 (file)
@@ -36,7 +36,6 @@ import org.sonar.api.utils.SonarException;
 import org.sonar.core.issue.ActionPlanStats;
 import org.sonar.core.issue.DefaultActionPlan;
 import org.sonar.core.issue.DefaultIssueBuilder;
-import org.sonar.core.issue.db.IssueChangeDto;
 import org.sonar.core.issue.workflow.Transition;
 import org.sonar.core.resource.ResourceDao;
 import org.sonar.core.resource.ResourceDto;
@@ -57,18 +56,20 @@ import java.util.Map;
 public class InternalRubyIssueService implements ServerComponent {
 
   private final IssueService issueService;
-  private final IssueChangeService changeService;
+  private final IssueCommentService commentService;
+  private final IssueChangelogService changelogService;
   private final ActionPlanService actionPlanService;
   private final IssueStatsFinder issueStatsFinder;
   private final ResourceDao resourceDao;
   private final ActionService actionService;
 
   public InternalRubyIssueService(IssueService issueService,
-                                  IssueChangeService changeService,
-                                  ActionPlanService actionPlanService,
+                                  IssueCommentService commentService,
+                                  IssueChangelogService changelogService, ActionPlanService actionPlanService,
                                   IssueStatsFinder issueStatsFinder, ResourceDao resourceDao, ActionService actionService) {
     this.issueService = issueService;
-    this.changeService = changeService;
+    this.commentService = commentService;
+    this.changelogService = changelogService;
     this.actionPlanService = actionPlanService;
     this.issueStatsFinder = issueStatsFinder;
     this.resourceDao = resourceDao;
@@ -95,9 +96,9 @@ public class InternalRubyIssueService implements ServerComponent {
     return Issue.RESOLUTIONS;
   }
 
-  public List<IssueChangeDto> changelog(String issueKey) {
+  public IssueChangelog changelog(String issueKey) {
     // TODO verify security
-    return changeService.changelog(issueKey);
+    return changelogService.changelog(issueKey, UserSession.get());
   }
 
   public Result<Issue> doTransition(String issueKey, String transitionKey) {
@@ -143,7 +144,7 @@ public class InternalRubyIssueService implements ServerComponent {
   public Result<IssueComment> addComment(String issueKey, String text) {
     Result<IssueComment> result = Result.of();
     try {
-      result.set(changeService.addComment(issueKey, text, UserSession.get()));
+      result.set(commentService.addComment(issueKey, text, UserSession.get()));
     } catch (Exception e) {
       result.addError(e.getMessage());
     }
@@ -151,15 +152,15 @@ public class InternalRubyIssueService implements ServerComponent {
   }
 
   public IssueComment deleteComment(String commentKey) {
-    return changeService.deleteComment(commentKey, UserSession.get());
+    return commentService.deleteComment(commentKey, UserSession.get());
   }
 
   public IssueComment editComment(String commentKey, String newText) {
-    return changeService.editComment(commentKey, newText, UserSession.get());
+    return commentService.editComment(commentKey, newText, UserSession.get());
   }
 
   public IssueComment findComment(String commentKey) {
-    return changeService.findComment(commentKey);
+    return commentService.findComment(commentKey);
   }
 
   /**
diff --git a/sonar-server/src/main/java/org/sonar/server/issue/IssueChangeService.java b/sonar-server/src/main/java/org/sonar/server/issue/IssueChangeService.java
deleted file mode 100644 (file)
index 3d73cbd..0000000
+++ /dev/null
@@ -1,137 +0,0 @@
-/*
- * 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.server.issue;
-
-import com.google.common.base.Objects;
-import com.google.common.base.Strings;
-import org.sonar.api.ServerComponent;
-import org.sonar.api.issue.IssueComment;
-import org.sonar.api.issue.IssueQuery;
-import org.sonar.api.issue.IssueQueryResult;
-import org.sonar.api.issue.internal.DefaultIssue;
-import org.sonar.api.issue.internal.DefaultIssueComment;
-import org.sonar.api.issue.internal.IssueChangeContext;
-import org.sonar.api.web.UserRole;
-import org.sonar.core.issue.IssueNotifications;
-import org.sonar.core.issue.IssueUpdater;
-import org.sonar.core.issue.db.IssueChangeDao;
-import org.sonar.core.issue.db.IssueChangeDto;
-import org.sonar.core.issue.db.IssueStorage;
-import org.sonar.server.user.UserSession;
-
-import java.util.Arrays;
-import java.util.Date;
-import java.util.List;
-
-public class IssueChangeService implements ServerComponent {
-
-  private final IssueUpdater updater;
-  private final IssueChangeDao changeDao;
-  private final IssueStorage storage;
-  private final DefaultIssueFinder finder;
-  private final IssueNotifications issueNotifications;
-
-  public IssueChangeService(IssueUpdater updater, IssueChangeDao changeDao, IssueStorage storage, DefaultIssueFinder finder, IssueNotifications issueNotifications) {
-    this.updater = updater;
-    this.changeDao = changeDao;
-    this.storage = storage;
-    this.finder = finder;
-    this.issueNotifications = issueNotifications;
-  }
-
-  public List<IssueChangeDto> changelog(String issueKey) {
-    // TODO verify security
-    return changeDao.selectIssueChangelog(issueKey);
-  }
-
-  public IssueComment findComment(String commentKey) {
-    return changeDao.selectCommentByKey(commentKey);
-  }
-
-  public IssueComment addComment(String issueKey, String text, UserSession userSession) {
-    verifyLoggedIn(userSession);
-
-    IssueQueryResult queryResult = loadIssue(issueKey);
-    DefaultIssue issue = (DefaultIssue) queryResult.first();
-
-    IssueChangeContext context = IssueChangeContext.createUser(new Date(), userSession.login());
-    updater.addComment(issue, text, context);
-    storage.save(issue);
-    issueNotifications.sendChanges(issue, context, queryResult, text);
-    return issue.comments().get(issue.comments().size() - 1);
-  }
-
-  public IssueComment deleteComment(String commentKey, UserSession userSession) {
-    DefaultIssueComment comment = changeDao.selectCommentByKey(commentKey);
-    if (comment == null) {
-      // TODO throw 404
-      throw new IllegalStateException();
-    }
-    if (Strings.isNullOrEmpty(comment.userLogin()) || !Objects.equal(comment.userLogin(), userSession.login())) {
-      // TODO throw unauthorized
-      throw new IllegalStateException();
-    }
-
-    // check authorization
-    finder.findByKey(comment.issueKey(), UserRole.USER);
-
-    changeDao.delete(commentKey);
-    return comment;
-  }
-
-  public IssueComment editComment(String commentKey, String text, UserSession userSession) {
-    DefaultIssueComment comment = changeDao.selectCommentByKey(commentKey);
-    if (comment == null) {
-      // TODO throw 404
-      throw new IllegalStateException();
-    }
-    if (Strings.isNullOrEmpty(comment.userLogin()) || !Objects.equal(comment.userLogin(), userSession.login())) {
-      // TODO throw unauthorized
-      throw new IllegalStateException();
-    }
-
-    // check authorization
-    finder.findByKey(comment.issueKey(), UserRole.USER);
-
-    IssueChangeDto dto = IssueChangeDto.of(comment);
-    dto.setUpdatedAt(new Date());
-    dto.setChangeData(text);
-    changeDao.update(dto);
-
-    return comment;
-  }
-
-  private void verifyLoggedIn(UserSession userSession) {
-    if (!userSession.isLoggedIn()) {
-      // must be logged
-      throw new IllegalStateException("User is not logged in");
-    }
-  }
-
-  // TODO remove this duplication from IssueService
-  public IssueQueryResult loadIssue(String issueKey) {
-    IssueQuery query = IssueQuery.builder().issueKeys(Arrays.asList(issueKey)).requiredRole(UserRole.USER).build();
-    IssueQueryResult result = finder.find(query);
-    if (result.issues().size()!=1) {
-      throw new IllegalStateException("Issue not found: " + issueKey);
-    }
-    return result;
-  }
-}
diff --git a/sonar-server/src/main/java/org/sonar/server/issue/IssueChangelog.java b/sonar-server/src/main/java/org/sonar/server/issue/IssueChangelog.java
new file mode 100644 (file)
index 0000000..5eb1e1a
--- /dev/null
@@ -0,0 +1,58 @@
+/*
+ * 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.server.issue;
+
+import com.google.common.collect.Maps;
+import org.sonar.api.issue.internal.FieldDiffs;
+import org.sonar.api.user.User;
+
+import javax.annotation.CheckForNull;
+import java.util.Collection;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * @since 3.6
+ */
+public class IssueChangelog {
+
+  private final List<FieldDiffs> changes;
+  private final Map<String, User> usersByLogin;
+
+  public IssueChangelog(List<FieldDiffs> changes, Collection<User> users) {
+    this.changes = changes;
+    this.usersByLogin = Maps.newHashMap();
+    for (User user : users) {
+      usersByLogin.put(user.login(), user);
+    }
+  }
+
+  public List<FieldDiffs> changes() {
+    return changes;
+  }
+
+  @CheckForNull
+  public User user(FieldDiffs change) {
+    if (change.userLogin() == null) {
+      return null;
+    }
+    return usersByLogin.get(change.userLogin());
+  }
+}
diff --git a/sonar-server/src/main/java/org/sonar/server/issue/IssueChangelogService.java b/sonar-server/src/main/java/org/sonar/server/issue/IssueChangelogService.java
new file mode 100644 (file)
index 0000000..3f9101e
--- /dev/null
@@ -0,0 +1,59 @@
+/*
+ * 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.server.issue;
+
+import com.google.common.collect.Lists;
+import org.sonar.api.ServerComponent;
+import org.sonar.api.issue.internal.FieldDiffs;
+import org.sonar.api.user.User;
+import org.sonar.api.user.UserFinder;
+import org.sonar.core.issue.db.IssueChangeDao;
+import org.sonar.server.user.UserSession;
+
+import java.util.Collection;
+import java.util.List;
+
+public class IssueChangelogService implements ServerComponent {
+
+  private final IssueChangeDao changeDao;
+  private final UserFinder userFinder;
+
+  public IssueChangelogService(IssueChangeDao changeDao, UserFinder userFinder) {
+    this.changeDao = changeDao;
+    this.userFinder = userFinder;
+  }
+
+  public IssueChangelog changelog(String issueKey, UserSession userSession) {
+    // TODO verify security
+    List<FieldDiffs> changes = changeDao.selectChangelogByIssue(issueKey);
+
+    // Load users
+    List<String> logins = Lists.newArrayList();
+    for (FieldDiffs change : changes) {
+      if (change.userLogin() != null) {
+        logins.add(change.userLogin());
+      }
+    }
+    Collection<User> users = userFinder.findByLogins(logins);
+
+    return new IssueChangelog(changes, users);
+
+  }
+}
diff --git a/sonar-server/src/main/java/org/sonar/server/issue/IssueCommentService.java b/sonar-server/src/main/java/org/sonar/server/issue/IssueCommentService.java
new file mode 100644 (file)
index 0000000..ba8962c
--- /dev/null
@@ -0,0 +1,131 @@
+/*
+ * 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.server.issue;
+
+import com.google.common.base.Objects;
+import com.google.common.base.Strings;
+import org.sonar.api.ServerComponent;
+import org.sonar.api.issue.IssueComment;
+import org.sonar.api.issue.IssueQuery;
+import org.sonar.api.issue.IssueQueryResult;
+import org.sonar.api.issue.internal.DefaultIssue;
+import org.sonar.api.issue.internal.DefaultIssueComment;
+import org.sonar.api.issue.internal.IssueChangeContext;
+import org.sonar.api.web.UserRole;
+import org.sonar.core.issue.IssueNotifications;
+import org.sonar.core.issue.IssueUpdater;
+import org.sonar.core.issue.db.IssueChangeDao;
+import org.sonar.core.issue.db.IssueChangeDto;
+import org.sonar.core.issue.db.IssueStorage;
+import org.sonar.server.user.UserSession;
+
+import java.util.Arrays;
+import java.util.Date;
+
+public class IssueCommentService implements ServerComponent {
+
+  private final IssueUpdater updater;
+  private final IssueChangeDao changeDao;
+  private final IssueStorage storage;
+  private final DefaultIssueFinder finder;
+  private final IssueNotifications issueNotifications;
+
+  public IssueCommentService(IssueUpdater updater, IssueChangeDao changeDao, IssueStorage storage, DefaultIssueFinder finder, IssueNotifications issueNotifications) {
+    this.updater = updater;
+    this.changeDao = changeDao;
+    this.storage = storage;
+    this.finder = finder;
+    this.issueNotifications = issueNotifications;
+  }
+
+  public IssueComment findComment(String commentKey) {
+    return changeDao.selectCommentByKey(commentKey);
+  }
+
+  public IssueComment addComment(String issueKey, String text, UserSession userSession) {
+    verifyLoggedIn(userSession);
+
+    IssueQueryResult queryResult = loadIssue(issueKey);
+    DefaultIssue issue = (DefaultIssue) queryResult.first();
+
+    IssueChangeContext context = IssueChangeContext.createUser(new Date(), userSession.login());
+    updater.addComment(issue, text, context);
+    storage.save(issue);
+    issueNotifications.sendChanges(issue, context, queryResult, text);
+    return issue.comments().get(issue.comments().size() - 1);
+  }
+
+  public IssueComment deleteComment(String commentKey, UserSession userSession) {
+    DefaultIssueComment comment = changeDao.selectCommentByKey(commentKey);
+    if (comment == null) {
+      // TODO throw 404
+      throw new IllegalStateException();
+    }
+    if (Strings.isNullOrEmpty(comment.userLogin()) || !Objects.equal(comment.userLogin(), userSession.login())) {
+      // TODO throw unauthorized
+      throw new IllegalStateException();
+    }
+
+    // check authorization
+    finder.findByKey(comment.issueKey(), UserRole.USER);
+
+    changeDao.delete(commentKey);
+    return comment;
+  }
+
+  public IssueComment editComment(String commentKey, String text, UserSession userSession) {
+    DefaultIssueComment comment = changeDao.selectCommentByKey(commentKey);
+    if (comment == null) {
+      // TODO throw 404
+      throw new IllegalStateException();
+    }
+    if (Strings.isNullOrEmpty(comment.userLogin()) || !Objects.equal(comment.userLogin(), userSession.login())) {
+      // TODO throw unauthorized
+      throw new IllegalStateException();
+    }
+
+    // check authorization
+    finder.findByKey(comment.issueKey(), UserRole.USER);
+
+    IssueChangeDto dto = IssueChangeDto.of(comment);
+    dto.setUpdatedAt(new Date());
+    dto.setChangeData(text);
+    changeDao.update(dto);
+
+    return comment;
+  }
+
+  private void verifyLoggedIn(UserSession userSession) {
+    if (!userSession.isLoggedIn()) {
+      // must be logged
+      throw new IllegalStateException("User is not logged in");
+    }
+  }
+
+  // TODO remove this duplication from IssueService
+  public IssueQueryResult loadIssue(String issueKey) {
+    IssueQuery query = IssueQuery.builder().issueKeys(Arrays.asList(issueKey)).requiredRole(UserRole.USER).build();
+    IssueQueryResult result = finder.find(query);
+    if (result.issues().size() != 1) {
+      throw new IllegalStateException("Issue not found: " + issueKey);
+    }
+    return result;
+  }
+}
index aaf8182ead1a2159e1405dd19abe03d52c7fd44f..8a52b6f8124e1b1082070071f5bac2350d8557c7 100644 (file)
@@ -263,12 +263,13 @@ public final class Platform {
     servicesContainer.addSingleton(FunctionExecutor.class);
     servicesContainer.addSingleton(IssueWorkflow.class);
     servicesContainer.addSingleton(IssueService.class);
-    servicesContainer.addSingleton(IssueChangeService.class);
+    servicesContainer.addSingleton(IssueCommentService.class);
     servicesContainer.addSingleton(DefaultIssueFinder.class);
     servicesContainer.addSingleton(IssueStatsFinder.class);
     servicesContainer.addSingleton(PublicRubyIssueService.class);
     servicesContainer.addSingleton(InternalRubyIssueService.class);
     servicesContainer.addSingleton(ActionPlanService.class);
+    servicesContainer.addSingleton(IssueChangelogService.class);
     servicesContainer.addSingleton(IssueNotifications.class);
     servicesContainer.addSingleton(ActionService.class);
     servicesContainer.addSingleton(Actions.class);
index 980c1bee0cdc185078dac8cd52c32ef3f6437836..bc5ae43b0292ed5993601507faa6ff2358598cb5 100644 (file)
@@ -193,7 +193,7 @@ class IssueController < ApplicationController
     require_parameters :id
     @issue_results = Api.issues.find(params[:id])
     @issue = @issue_results.first()
-    @changes = Internal.issues.changelog(params[:id])
+    @changelog = Internal.issues.changelog(params[:id])
     render :partial => 'issue/changelog'
   end
 
index 421d203caad4bd7c279f267ce66f5697360e2d74..26c12118c091b74f1c803ddbf064226cdc9031de 100644 (file)
@@ -1,15 +1,35 @@
-<ul>
-  <li>
-    <%= format_datetime(@issue.creationDate()) -%>
-    <% if @issue.reporter %>
-    - <%= message('issue.reported_by') -%> <%= @issue_results.user(@issue.reporter).name -%>
-    <% end %>
-    - <%= message('created') -%>
-  </li>
-
-  <% @changes.each do |change| %>
-  <li>
-    <%= format_datetime(change.createdAt()) -%> - <%= change.userLogin -%> - <%= change.changeData -%>
-  </li>
+<table class="spaced">
+  <tr>
+    <td class="thin left top" nowrap><%= format_datetime(@issue.creationDate()) -%></td>
+    <td class="thin left top"nowrap><%= @issue_results.user(@issue.reporter).name if @issue.reporter -%></td>
+    <td class="left top"><%= message('created') -%></td>
+  </tr>
+  <%
+     @changelog.changes.each do |change|
+       user = @changelog.user(change)
+  %>
+    <tr>
+      <td class="thin left top" nowrap><%= format_datetime(change.createdAt()) -%></td>
+      <td class="thin left top" nowrap><%= h(user.name()) if user -%></td>
+      <td class="left top">
+        <%
+           change.diffs.entrySet().each_with_index do |entry, index|
+             key = entry.getKey()
+             diff = entry.getValue()
+        %>
+          <% if index>0 %><br/><% end %>
+          <% if diff.newValue.present? %>
+            <%= message('issue.changelog.changed_to', :params => [message(key), diff.newValue()]) -%>
+          <% else %>
+            <%= message('issue.changelog.removed', :params => [message(key)]) -%>
+          <% end %>
+          <% if diff.oldValue.present? %>
+            (<%= message('issue.changelog.was', :params => [diff.oldValue]) -%>)
+          <% end %>
+        <% end %>
+      </td>
+    </tr>
   <% end %>
-</ul>
\ No newline at end of file
+</table>
+
+
index 7c3b38d610e177256f8584eb0a2d3bb9a8c2de7f..93cf0d33881379697d45e105a04325b454b6a1d2 100644 (file)
@@ -24,12 +24,15 @@ import org.junit.Before;
 import org.junit.Test;
 import org.mockito.ArgumentCaptor;
 import org.sonar.api.issue.ActionPlan;
+import org.sonar.api.issue.internal.FieldDiffs;
+import org.sonar.api.user.User;
 import org.sonar.core.issue.DefaultActionPlan;
 import org.sonar.core.resource.ResourceDao;
 import org.sonar.core.resource.ResourceDto;
 import org.sonar.core.resource.ResourceQuery;
 import org.sonar.server.user.UserSession;
 
+import java.util.Collections;
 import java.util.Map;
 
 import static com.google.common.collect.Maps.newHashMap;
@@ -39,19 +42,20 @@ import static org.mockito.Mockito.*;
 
 public class InternalRubyIssueServiceTest {
 
-  private InternalRubyIssueService internalRubyIssueService;
-  private IssueService issueService = mock(IssueService.class);
-  private IssueChangeService commentService = mock(IssueChangeService.class);
-  private ActionPlanService actionPlanService = mock(ActionPlanService.class);
-  private ResourceDao resourceDao = mock(ResourceDao.class);
-  private IssueStatsFinder issueStatsFinder = mock(IssueStatsFinder.class);
-  private ActionService actionService = mock(ActionService.class);
+  InternalRubyIssueService service;
+  IssueService issueService = mock(IssueService.class);
+  IssueCommentService commentService = mock(IssueCommentService.class);
+  IssueChangelogService changelogService = mock(IssueChangelogService.class);
+  ActionPlanService actionPlanService = mock(ActionPlanService.class);
+  ResourceDao resourceDao = mock(ResourceDao.class);
+  IssueStatsFinder issueStatsFinder = mock(IssueStatsFinder.class);
+  ActionService actionService = mock(ActionService.class);
 
   @Before
-  public void before() {
+  public void setUp() {
     ResourceDto project = new ResourceDto().setKey("org.sonar.Sample");
     when(resourceDao.getResource(any(ResourceQuery.class))).thenReturn(project);
-    internalRubyIssueService = new InternalRubyIssueService(issueService, commentService, actionPlanService, issueStatsFinder, resourceDao, actionService);
+    service = new InternalRubyIssueService(issueService, commentService, changelogService, actionPlanService, issueStatsFinder, resourceDao, actionService);
   }
 
   @Test
@@ -63,7 +67,7 @@ public class InternalRubyIssueServiceTest {
     parameters.put("deadLine", "2113-05-13");
 
     ArgumentCaptor<ActionPlan> actionPlanCaptor = ArgumentCaptor.forClass(ActionPlan.class);
-    Result result = internalRubyIssueService.createActionPlan(parameters);
+    Result result = service.createActionPlan(parameters);
     assertThat(result.ok()).isTrue();
 
     verify(actionPlanService).create(actionPlanCaptor.capture(), any(UserSession.class));
@@ -87,7 +91,7 @@ public class InternalRubyIssueServiceTest {
     parameters.put("project", "org.sonar.MultiSample");
 
     ArgumentCaptor<ActionPlan> actionPlanCaptor = ArgumentCaptor.forClass(ActionPlan.class);
-    Result result = internalRubyIssueService.updateActionPlan("ABCD", parameters);
+    Result result = service.updateActionPlan("ABCD", parameters);
     assertThat(result.ok()).isTrue();
 
     verify(actionPlanService).update(actionPlanCaptor.capture(), any(UserSession.class));
@@ -110,7 +114,7 @@ public class InternalRubyIssueServiceTest {
     parameters.put("deadLine", "2113-05-13");
 
     ArgumentCaptor<ActionPlan> actionPlanCaptor = ArgumentCaptor.forClass(ActionPlan.class);
-    Result result = internalRubyIssueService.updateActionPlan("ABCD", parameters);
+    Result result = service.updateActionPlan("ABCD", parameters);
     assertThat(result.ok()).isTrue();
 
     verify(actionPlanService).update(actionPlanCaptor.capture(), any(UserSession.class));
@@ -128,7 +132,7 @@ public class InternalRubyIssueServiceTest {
   public void should_not_update_action_plan_when_action_plan_is_not_found() {
     when(actionPlanService.findByKey(eq("ABCD"), any(UserSession.class))).thenReturn(null);
 
-    Result result = internalRubyIssueService.updateActionPlan("ABCD", null);
+    Result result = service.updateActionPlan("ABCD", null);
     assertThat(result.ok()).isFalse();
     assertThat(result.errors()).contains(Result.Message.ofL10n("action_plans.errors.action_plan_does_not_exist", "ABCD"));
   }
@@ -137,7 +141,7 @@ public class InternalRubyIssueServiceTest {
   public void should_delete_action_plan() {
     when(actionPlanService.findByKey(eq("ABCD"), any(UserSession.class))).thenReturn(DefaultActionPlan.create("Long term"));
 
-    Result result = internalRubyIssueService.deleteActionPlan("ABCD");
+    Result result = service.deleteActionPlan("ABCD");
     verify(actionPlanService).delete(eq("ABCD"), any(UserSession.class));
     assertThat(result.ok()).isTrue();
   }
@@ -146,7 +150,7 @@ public class InternalRubyIssueServiceTest {
   public void should_not_delete_action_plan_if_action_plan_not_found() {
     when(actionPlanService.findByKey(eq("ABCD"), any(UserSession.class))).thenReturn(null);
 
-    Result result = internalRubyIssueService.deleteActionPlan("ABCD");
+    Result result = service.deleteActionPlan("ABCD");
     verify(actionPlanService, never()).delete(eq("ABCD"), any(UserSession.class));
     assertThat(result.ok()).isFalse();
     assertThat(result.errors()).contains(Result.Message.ofL10n("action_plans.errors.action_plan_does_not_exist", "ABCD"));
@@ -156,7 +160,7 @@ public class InternalRubyIssueServiceTest {
   public void should_close_action_plan() {
     when(actionPlanService.findByKey(eq("ABCD"), any(UserSession.class))).thenReturn(DefaultActionPlan.create("Long term"));
 
-    Result result = internalRubyIssueService.closeActionPlan("ABCD");
+    Result result = service.closeActionPlan("ABCD");
     verify(actionPlanService).setStatus(eq("ABCD"), eq("CLOSED"), any(UserSession.class));
     assertThat(result.ok()).isTrue();
   }
@@ -165,7 +169,7 @@ public class InternalRubyIssueServiceTest {
   public void should_open_action_plan() {
     when(actionPlanService.findByKey(eq("ABCD"), any(UserSession.class))).thenReturn(DefaultActionPlan.create("Long term"));
 
-    Result result = internalRubyIssueService.openActionPlan("ABCD");
+    Result result = service.openActionPlan("ABCD");
     verify(actionPlanService).setStatus(eq("ABCD"), eq("OPEN"), any(UserSession.class));
     assertThat(result.ok()).isTrue();
   }
@@ -176,7 +180,7 @@ public class InternalRubyIssueServiceTest {
     parameters.put("name", "Long term");
     parameters.put("description", "Long term issues");
 
-    Result result = internalRubyIssueService.createActionPlanResult(parameters);
+    Result result = service.createActionPlanResult(parameters);
     assertThat(result.ok()).isFalse();
     assertThat(result.errors()).contains(Result.Message.ofL10n("errors.cant_be_empty", "project"));
   }
@@ -188,7 +192,7 @@ public class InternalRubyIssueServiceTest {
     parameters.put("description", "Long term issues");
     parameters.put("project", "org.sonar.Sample");
 
-    Result result = internalRubyIssueService.createActionPlanResult(parameters);
+    Result result = service.createActionPlanResult(parameters);
     assertThat(result.ok()).isFalse();
     assertThat(result.errors()).contains(Result.Message.ofL10n("errors.cant_be_empty", "name"));
   }
@@ -200,7 +204,7 @@ public class InternalRubyIssueServiceTest {
     parameters.put("description", "Long term issues");
     parameters.put("project", "org.sonar.Sample");
 
-    Result result = internalRubyIssueService.createActionPlanResult(parameters);
+    Result result = service.createActionPlanResult(parameters);
     assertThat(result.ok()).isFalse();
     assertThat(result.errors()).contains(Result.Message.ofL10n("errors.is_too_long", "name", 200));
   }
@@ -212,7 +216,7 @@ public class InternalRubyIssueServiceTest {
     parameters.put("description", createLongString(1001));
     parameters.put("project", "org.sonar.Sample");
 
-    Result result = internalRubyIssueService.createActionPlanResult(parameters);
+    Result result = service.createActionPlanResult(parameters);
     assertThat(result.ok()).isFalse();
     assertThat(result.errors()).contains(Result.Message.ofL10n("errors.is_too_long", "description", 1000));
   }
@@ -225,7 +229,7 @@ public class InternalRubyIssueServiceTest {
     parameters.put("project", "org.sonar.Sample");
     parameters.put("deadLine", "18/05/2013");
 
-    Result result = internalRubyIssueService.createActionPlanResult(parameters);
+    Result result = service.createActionPlanResult(parameters);
     assertThat(result.ok()).isFalse();
     assertThat(result.errors()).contains(Result.Message.ofL10n("errors.is_not_valid", "date"));
   }
@@ -238,7 +242,7 @@ public class InternalRubyIssueServiceTest {
     parameters.put("project", "org.sonar.Sample");
     parameters.put("deadLine", "2000-01-01");
 
-    Result result = internalRubyIssueService.createActionPlanResult(parameters);
+    Result result = service.createActionPlanResult(parameters);
     assertThat(result.ok()).isFalse();
     assertThat(result.errors()).contains(Result.Message.ofL10n("action_plans.date_cant_be_in_past"));
   }
@@ -252,7 +256,7 @@ public class InternalRubyIssueServiceTest {
 
     when(actionPlanService.isNameAlreadyUsedForProject(anyString(), anyString())).thenReturn(true);
 
-    Result result = internalRubyIssueService.createActionPlanResult(parameters, DefaultActionPlan.create("Short term"));
+    Result result = service.createActionPlanResult(parameters, DefaultActionPlan.create("Short term"));
     assertThat(result.ok()).isFalse();
     assertThat(result.errors()).contains(Result.Message.ofL10n("action_plans.same_name_in_same_project"));
   }
@@ -266,10 +270,11 @@ public class InternalRubyIssueServiceTest {
 
     when(resourceDao.getResource(any(ResourceQuery.class))).thenReturn(null);
 
-    Result result = internalRubyIssueService.createActionPlanResult(parameters);
+    Result result = service.createActionPlanResult(parameters);
     assertThat(result.ok()).isFalse();
     assertThat(result.errors()).contains(Result.Message.ofL10n("action_plans.errors.project_does_not_exist", "org.sonar.Sample"));
   }
+
   public String createLongString(int size) {
     String result = "";
     for (int i = 0; i < size; i++) {
@@ -277,4 +282,14 @@ public class InternalRubyIssueServiceTest {
     }
     return result;
   }
+
+  @Test
+  public void test_changelog() throws Exception {
+    IssueChangelog changelog = new IssueChangelog(Collections.<FieldDiffs>emptyList(), Collections.<User>emptyList());
+    when(changelogService.changelog(eq("ABCDE"), any(UserSession.class))).thenReturn(changelog);
+
+    IssueChangelog result = service.changelog("ABCDE");
+
+    assertThat(result).isSameAs(changelog);
+  }
 }
diff --git a/sonar-server/src/test/java/org/sonar/server/issue/IssueChangelogServiceTest.java b/sonar-server/src/test/java/org/sonar/server/issue/IssueChangelogServiceTest.java
new file mode 100644 (file)
index 0000000..0e517c8
--- /dev/null
@@ -0,0 +1,57 @@
+/*
+ * 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.server.issue;
+
+import org.junit.Test;
+import org.sonar.api.issue.internal.FieldDiffs;
+import org.sonar.api.user.User;
+import org.sonar.api.user.UserFinder;
+import org.sonar.core.issue.db.IssueChangeDao;
+import org.sonar.core.user.DefaultUser;
+import org.sonar.server.user.UserSession;
+
+import java.util.Arrays;
+
+import static org.fest.assertions.Assertions.assertThat;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+
+public class IssueChangelogServiceTest {
+
+  IssueChangeDao changeDao = mock(IssueChangeDao.class);
+  UserFinder userFinder = mock(UserFinder.class);
+  IssueChangelogService service = new IssueChangelogService(changeDao, userFinder);
+
+  @Test
+  public void should_load_changelog_and_related_users() throws Exception {
+    FieldDiffs userChange = new FieldDiffs().setUserLogin("arthur").setDiff("severity", "MAJOR", "BLOCKER");
+    FieldDiffs scanChange = new FieldDiffs().setDiff("status", "RESOLVED", "CLOSED");
+    when(changeDao.selectChangelogByIssue("ABCDE")).thenReturn(Arrays.asList(userChange, scanChange));
+    User arthur = new DefaultUser().setLogin("arthur").setName("Arthur");
+    when(userFinder.findByLogins(Arrays.asList("arthur"))).thenReturn(Arrays.asList(arthur));
+
+    IssueChangelog changelog = service.changelog("ABCDE", mock(UserSession.class));
+
+    assertThat(changelog).isNotNull();
+    assertThat(changelog.changes()).containsOnly(userChange, scanChange);
+    assertThat(changelog.user(scanChange)).isNull();
+    assertThat(changelog.user(userChange)).isSameAs(arthur);
+  }
+}