From: Julien Lancelot Date: Wed, 7 Oct 2015 12:12:10 +0000 (+0200) Subject: SONAR-6717 Restore issues actions API X-Git-Tag: 5.2-RC1~15 X-Git-Url: https://source.dussan.org/?a=commitdiff_plain;h=f59052c8e1daed111d931893b2a08f9f5fa2a74a;p=sonarqube.git SONAR-6717 Restore issues actions API --- diff --git a/it/it-plugins/issue-action-plugin/pom.xml b/it/it-plugins/issue-action-plugin/pom.xml new file mode 100644 index 00000000000..6b176b8e5bd --- /dev/null +++ b/it/it-plugins/issue-action-plugin/pom.xml @@ -0,0 +1,40 @@ + + + 4.0.0 + + + org.sonarsource.it + it-plugins + 5.2-SNAPSHOT + + + issue-action-plugin + sonar-plugin + Plugins :: Issue Action + 1.0-SNAPSHOT + + + + org.codehaus.sonar + sonar-plugin-api + ${apiVersion} + provided + + + + + + + org.codehaus.sonar + sonar-packaging-maven-plugin + 1.1 + true + + IssueActionPlugin + issueaction + + + + + diff --git a/it/it-plugins/issue-action-plugin/src/main/java/ActionDefinition.java b/it/it-plugins/issue-action-plugin/src/main/java/ActionDefinition.java new file mode 100644 index 00000000000..84f5e6c5a6a --- /dev/null +++ b/it/it-plugins/issue-action-plugin/src/main/java/ActionDefinition.java @@ -0,0 +1,46 @@ +/* + * SonarQube, open source software quality management tool. + * Copyright (C) 2008-2014 SonarSource + * mailto:contact AT sonarsource DOT com + * + * SonarQube is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * SonarQube is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +import org.sonar.api.ServerExtension; +import org.sonar.api.issue.Issue; +import org.sonar.api.issue.action.Actions; +import org.sonar.api.issue.action.Function; +import org.sonar.api.issue.condition.HasResolution; + +public class ActionDefinition implements ServerExtension { + + private final Actions actions; + + public ActionDefinition(Actions actions) { + this.actions = actions; + } + + public void start() { + actions.add("fake") + .setConditions(new HasResolution(Issue.RESOLUTION_FIXED)) + .setFunctions(new Function() { + @Override + public void execute(Context context) { + context.setAttribute("fake", "fake action"); + context.addComment("New Comment from fake action"); + } + }); + } +} diff --git a/it/it-plugins/issue-action-plugin/src/main/java/IssueActionPlugin.java b/it/it-plugins/issue-action-plugin/src/main/java/IssueActionPlugin.java new file mode 100644 index 00000000000..d322d9f4d90 --- /dev/null +++ b/it/it-plugins/issue-action-plugin/src/main/java/IssueActionPlugin.java @@ -0,0 +1,34 @@ +/* + * SonarQube, open source software quality management tool. + * Copyright (C) 2008-2014 SonarSource + * mailto:contact AT sonarsource DOT com + * + * SonarQube is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * SonarQube is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +import org.sonar.api.SonarPlugin; + +import java.util.Arrays; +import java.util.List; + +public class IssueActionPlugin extends SonarPlugin { + + public List getExtensions() { + return Arrays.asList( + ActionDefinition.class + ); + } + +} diff --git a/it/it-plugins/issue-action-plugin/src/main/resources/org/sonar/l10n/issueaction.properties b/it/it-plugins/issue-action-plugin/src/main/resources/org/sonar/l10n/issueaction.properties new file mode 100644 index 00000000000..f507ceed575 --- /dev/null +++ b/it/it-plugins/issue-action-plugin/src/main/resources/org/sonar/l10n/issueaction.properties @@ -0,0 +1,21 @@ +# +# SonarQube, open source software quality management tool. +# Copyright (C) 2008-2014 SonarSource +# mailto:contact AT sonarsource DOT com +# +# SonarQube is free software; you can redistribute it and/or +# modify it under the terms of the GNU Lesser General Public +# License as published by the Free Software Foundation; either +# version 3 of the License, or (at your option) any later version. +# +# SonarQube is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +# Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public License +# along with this program; if not, write to the Free Software Foundation, +# Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +# + +issue.action.fake.formlink=Fake diff --git a/it/it-plugins/pom.xml b/it/it-plugins/pom.xml index b0d7c374cef..984e022859d 100644 --- a/it/it-plugins/pom.xml +++ b/it/it-plugins/pom.xml @@ -35,6 +35,7 @@ batch-plugin extension-lifecycle-plugin global-property-change-plugin + issue-action-plugin l10n-fr-pack license-plugin project-builder-plugin diff --git a/it/it-tests/src/test/java/issue/suite/IssueActionTest.java b/it/it-tests/src/test/java/issue/suite/IssueActionTest.java new file mode 100644 index 00000000000..ab23ce589fe --- /dev/null +++ b/it/it-tests/src/test/java/issue/suite/IssueActionTest.java @@ -0,0 +1,257 @@ +/* + * SonarQube, open source software quality management tool. + * Copyright (C) 2008-2014 SonarSource + * mailto:contact AT sonarsource DOT com + * + * SonarQube is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * SonarQube is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +package issue.suite; + +import com.sonar.orchestrator.Orchestrator; +import com.sonar.orchestrator.build.SonarRunner; +import com.sonar.orchestrator.locator.FileLocation; +import java.util.List; +import org.junit.Before; +import org.junit.ClassRule; +import org.junit.Ignore; +import org.junit.Test; +import org.sonar.wsclient.base.HttpException; +import org.sonar.wsclient.issue.ActionPlan; +import org.sonar.wsclient.issue.ActionPlanClient; +import org.sonar.wsclient.issue.Issue; +import org.sonar.wsclient.issue.IssueComment; +import org.sonar.wsclient.issue.IssueQuery; +import org.sonar.wsclient.issue.Issues; +import org.sonar.wsclient.issue.NewActionPlan; + +import static issue.suite.IssueTestSuite.ORCHESTRATOR; +import static issue.suite.IssueTestSuite.adminIssueClient; +import static issue.suite.IssueTestSuite.issueClient; +import static issue.suite.IssueTestSuite.search; +import static issue.suite.IssueTestSuite.searchIssueByKey; +import static issue.suite.IssueTestSuite.searchIssues; +import static issue.suite.IssueTestSuite.searchRandomIssue; +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.Assert.fail; +import static util.ItUtils.projectDir; +import static util.ItUtils.toDate; +import static util.ItUtils.verifyHttpException; + +public class IssueActionTest { + + @ClassRule + public static Orchestrator orchestrator = ORCHESTRATOR; + + Issue issue; + SonarRunner scan; + + private static List searchIssuesBySeverities(String componentKey, String... severities) { + return searchIssues(IssueQuery.create().componentRoots(componentKey).severities(severities)); + } + + private static ActionPlanClient adminActionPlanClient() { + return orchestrator.getServer().adminWsClient().actionPlanClient(); + } + + @Before + public void resetData() { + orchestrator.resetData(); + orchestrator.getServer().restoreProfile(FileLocation.ofClasspath("/issue/suite/IssueActionTest/xoo-one-issue-per-line-profile.xml")); + orchestrator.getServer().provisionProject("sample", "Sample"); + orchestrator.getServer().associateProjectToQualityProfile("sample", "xoo", "xoo-one-issue-per-line-profile"); + + scan = SonarRunner.create(projectDir("shared/xoo-sample")); + orchestrator.executeBuild(scan); + issue = searchRandomIssue(); + } + + @Test + public void no_comments_by_default() throws Exception { + assertThat(issue.comments()).isEmpty(); + } + + @Test + public void add_comment() throws Exception { + IssueComment comment = adminIssueClient().addComment(issue.key(), "this is my *comment*"); + assertThat(comment.key()).isNotNull(); + assertThat(comment.htmlText()).isEqualTo("this is my comment"); + assertThat(comment.login()).isEqualTo("admin"); + assertThat(comment.createdAt()).isNotNull(); + + // reload issue + Issue reloaded = searchIssueByKey(issue.key()); + + assertThat(reloaded.comments()).hasSize(1); + assertThat(reloaded.comments().get(0).key()).isEqualTo(comment.key()); + assertThat(reloaded.comments().get(0).htmlText()).isEqualTo("this is my comment"); + assertThat(reloaded.updateDate().before(issue.creationDate())).isFalse(); + } + + /** + * SONAR-4450 + */ + @Test + public void should_reject_blank_comment() throws Exception { + try { + adminIssueClient().addComment(issue.key(), " "); + fail(); + } catch (HttpException ex) { + assertThat(ex.status()).isEqualTo(400); + } + + Issue reloaded = searchIssueByKey(issue.key()); + assertThat(reloaded.comments()).hasSize(0); + } + + /** + * SONAR-4352 + */ + @Test + public void change_severity() { + String componentKey = "sample"; + + // there are no blocker issues + assertThat(searchIssuesBySeverities(componentKey, "BLOCKER")).isEmpty(); + + // increase the severity of an issue + adminIssueClient().setSeverity(issue.key(), "BLOCKER"); + + assertThat(searchIssuesBySeverities(componentKey, "BLOCKER")).hasSize(1); + + orchestrator.executeBuild(scan); + Issue reloaded = searchIssueByKey(issue.key()); + assertThat(reloaded.severity()).isEqualTo("BLOCKER"); + assertThat(reloaded.status()).isEqualTo("OPEN"); + assertThat(reloaded.resolution()).isNull(); + assertThat(reloaded.creationDate()).isEqualTo(issue.creationDate()); + assertThat(reloaded.creationDate().before(reloaded.updateDate())).isTrue(); + } + + /** + * SONAR-4287 + */ + @Test + public void assign() { + assertThat(issue.assignee()).isNull(); + Issues issues = search(IssueQuery.create().issues(issue.key())); + assertThat(issues.users()).isEmpty(); + + adminIssueClient().assign(issue.key(), "admin"); + assertThat(search(IssueQuery.create().assignees("admin")).list()).hasSize(1); + + orchestrator.executeBuild(scan); + Issue reloaded = searchIssueByKey(issue.key()); + assertThat(reloaded.assignee()).isEqualTo("admin"); + assertThat(reloaded.creationDate()).isEqualTo(issue.creationDate()); + + issues = search(IssueQuery.create().issues(issue.key())); + assertThat(issues.user("admin")).isNotNull(); + assertThat(issues.user("admin").name()).isEqualTo("Administrator"); + + // unassign + adminIssueClient().assign(issue.key(), null); + reloaded = searchIssueByKey(issue.key()); + assertThat(reloaded.assignee()).isNull(); + assertThat(issueClient().find(IssueQuery.create().assignees("admin")).list()).isEmpty(); + } + + /** + * SONAR-4287 + */ + @Test + public void fail_assign_if_assignee_does_not_exist() { + assertThat(issue.assignee()).isNull(); + try { + adminIssueClient().assign(issue.key(), "unknown"); + fail(); + } catch (Exception e) { + verifyHttpException(e, 400); + } + } + + /** + * SONAR-4290 + */ + @Test + public void plan() { + assertThat(issue.actionPlan()).isNull(); + + // Set action plan to issue + ActionPlan newActionPlan = adminActionPlanClient().create(NewActionPlan.create().name("Short term").project("sample") + .description("Short term issues").deadLine(toDate("2113-01-31"))); + assertThat(newActionPlan.key()).isNotNull(); + adminIssueClient().plan(issue.key(), newActionPlan.key()); + assertThat(search(IssueQuery.create().actionPlans(newActionPlan.key())).list()).hasSize(1); + + orchestrator.executeBuild(scan); + Issue reloaded = searchIssueByKey(issue.key()); + assertThat(reloaded.actionPlan()).isEqualTo(newActionPlan.key()); + assertThat(reloaded.creationDate()).isEqualTo(issue.creationDate()); + ActionPlan actionPlan = search(IssueQuery.create().actionPlans(newActionPlan.key())).actionPlans(reloaded); + assertThat(actionPlan.name()).isEqualTo(newActionPlan.name()); + assertThat(actionPlan.deadLine()).isEqualTo(newActionPlan.deadLine()); + } + + @Test + public void fail_plan_if_action_plan_does_not_exist() { + assertThat(issue.actionPlan()).isNull(); + try { + adminIssueClient().plan(issue.key(), "unknown"); + fail(); + } catch (Exception e) { + verifyHttpException(e, 400); + } + } + + @Test + public void unplan() { + assertThat(issue.actionPlan()).isNull(); + + // Set action plan to issue + ActionPlan newActionPlan = adminActionPlanClient().create(NewActionPlan.create().name("Short term").project("sample") + .description("Short term issues").deadLine(toDate("2113-01-31"))); + assertThat(newActionPlan.key()).isNotNull(); + adminIssueClient().plan(issue.key(), newActionPlan.key()); + assertThat(search(IssueQuery.create().actionPlans(newActionPlan.key())).list()).hasSize(1); + + // Unplan + adminIssueClient().plan(issue.key(), null); + assertThat(search(IssueQuery.create().actionPlans(newActionPlan.key())).list()).hasSize(0); + + orchestrator.executeBuild(scan); + Issue reloaded = searchIssueByKey(issue.key()); + assertThat(reloaded.actionPlan()).isNull(); + assertThat(reloaded.creationDate()).isEqualTo(issue.creationDate()); + } + + /** + * SONAR-4315 + */ + @Test + public void apply_action_from_plugin() { + // The condition on the action defined by the plugin is that the status must be resolved + adminIssueClient().doTransition(issue.key(), "resolve"); + assertThat(adminIssueClient().actions(issue.key())).contains("fake"); + + adminIssueClient().doAction(issue.key(), "fake"); + + // reload issue + Issue reloaded = searchIssueByKey(issue.key()); + + assertThat(reloaded.comments()).hasSize(1); + assertThat(reloaded.comments().get(0).htmlText()).isEqualTo("New Comment from fake action"); + } + +} diff --git a/it/it-tests/src/test/java/issue/suite/IssueTestSuite.java b/it/it-tests/src/test/java/issue/suite/IssueTestSuite.java index 6757552276e..3e6a1e4ce02 100644 --- a/it/it-tests/src/test/java/issue/suite/IssueTestSuite.java +++ b/it/it-tests/src/test/java/issue/suite/IssueTestSuite.java @@ -13,42 +13,52 @@ import org.junit.runners.Suite; import org.sonar.wsclient.issue.Issue; import org.sonar.wsclient.issue.IssueClient; import org.sonar.wsclient.issue.IssueQuery; -import util.ItUtils; +import org.sonar.wsclient.issue.Issues; import static org.assertj.core.api.Assertions.assertThat; +import static util.ItUtils.pluginArtifact; +import static util.ItUtils.xooPlugin; @RunWith(Suite.class) @Suite.SuiteClasses({ - CommonRulesTest.class, IssueWorkflowTest.class, ManualRulesTest.class, CustomRulesTest.class + CommonRulesTest.class, IssueWorkflowTest.class, ManualRulesTest.class, CustomRulesTest.class, IssueActionTest.class }) public class IssueTestSuite { @ClassRule public static final Orchestrator ORCHESTRATOR = Orchestrator.builderEnv() .setSonarVersion("DEV") - .addPlugin(ItUtils.xooPlugin()) + .addPlugin(xooPlugin()) + .addPlugin(pluginArtifact("issue-action-plugin")) .build(); static IssueClient adminIssueClient() { return ORCHESTRATOR.getServer().adminWsClient().issueClient(); } + static IssueClient issueClient() { + return ORCHESTRATOR.getServer().wsClient().issueClient(); + } + static Issue searchRandomIssue() { List issues = searchIssues(IssueQuery.create()); assertThat(issues).isNotEmpty(); return issues.get(0); } - static List searchIssues(IssueQuery issueQuery) { + static Issues search(IssueQuery issueQuery) { issueQuery.urlParams().put("additionalFields", "_all"); - return adminIssueClient().find(issueQuery).list(); + return issueClient().find(issueQuery); + } + + static List searchIssues(IssueQuery issueQuery) { + return search(issueQuery).list(); } static Issue searchIssueByKey(String issueKey) { - IssueQuery query = IssueQuery.create().issues(issueKey); - query.urlParams().put("additionalFields", "_all"); - List issues = searchIssues(query); + List issues = searchIssues(IssueQuery.create().issues(issueKey)); assertThat(issues).hasSize(1); return issues.get(0); } + } diff --git a/it/it-tests/src/test/java/util/ItUtils.java b/it/it-tests/src/test/java/util/ItUtils.java index 1d8e3406ea8..4cb4df2c045 100644 --- a/it/it-tests/src/test/java/util/ItUtils.java +++ b/it/it-tests/src/test/java/util/ItUtils.java @@ -9,6 +9,9 @@ import com.google.common.collect.Iterables; import com.sonar.orchestrator.Orchestrator; import com.sonar.orchestrator.build.BuildResult; import com.sonar.orchestrator.build.SonarRunner; +import java.text.ParseException; +import java.text.SimpleDateFormat; +import java.util.Date; import org.json.simple.JSONArray; import org.json.simple.JSONObject; import org.json.simple.JSONValue; @@ -27,6 +30,7 @@ import static org.assertj.core.api.Assertions.fail; import static org.assertj.core.api.Assertions.assertThat; import org.apache.commons.io.FileUtils; +import org.sonar.wsclient.base.HttpException; public class ItUtils { @@ -157,4 +161,20 @@ public class ItUtils { } return from(Iterables.concat(asList(properties), asList(str))).toArray(String.class); } + + public static void verifyHttpException(Exception e, int expectedCode) { + assertThat(e).isInstanceOf(HttpException.class); + HttpException exception = (HttpException) e; + assertThat(exception.status()).isEqualTo(expectedCode); + } + + public static Date toDate(String sDate) { + try { + SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd"); + return sdf.parse(sDate); + } catch (ParseException e) { + throw new RuntimeException(e); + } + } + } diff --git a/it/it-tests/src/test/resources/issue/suite/IssueActionTest/xoo-one-issue-per-line-profile.xml b/it/it-tests/src/test/resources/issue/suite/IssueActionTest/xoo-one-issue-per-line-profile.xml new file mode 100644 index 00000000000..608f80cae96 --- /dev/null +++ b/it/it-tests/src/test/resources/issue/suite/IssueActionTest/xoo-one-issue-per-line-profile.xml @@ -0,0 +1,12 @@ + + + xoo-one-issue-per-line-profile + xoo + + + xoo + OneIssuePerLine + CRITICAL + + + diff --git a/server/sonar-server/src/main/java/org/sonar/server/issue/ActionService.java b/server/sonar-server/src/main/java/org/sonar/server/issue/ActionService.java index 42da9fd1b8e..c57be962187 100644 --- a/server/sonar-server/src/main/java/org/sonar/server/issue/ActionService.java +++ b/server/sonar-server/src/main/java/org/sonar/server/issue/ActionService.java @@ -20,13 +20,32 @@ package org.sonar.server.issue; +import com.google.common.base.Predicate; +import com.google.common.base.Strings; +import java.util.Date; import java.util.List; +import javax.annotation.Nonnull; +import javax.annotation.Nullable; +import org.sonar.api.config.Settings; +import org.sonar.api.issue.Issue; +import org.sonar.api.issue.action.Action; +import org.sonar.api.issue.action.Actions; +import org.sonar.api.issue.action.Function; import org.sonar.api.server.ServerSide; -import org.sonar.api.web.UserRole; -import org.sonar.db.issue.IssueDto; +import org.sonar.core.issue.DefaultIssue; +import org.sonar.core.issue.IssueChangeContext; +import org.sonar.core.issue.IssueUpdater; +import org.sonar.db.DbClient; +import org.sonar.db.DbSession; +import org.sonar.server.properties.ProjectSettingsFactory; import org.sonar.server.user.UserSession; +import static com.google.common.base.Preconditions.checkArgument; +import static com.google.common.base.Preconditions.checkState; +import static com.google.common.collect.FluentIterable.from; +import static com.google.common.collect.Iterables.find; import static com.google.common.collect.Lists.newArrayList; +import static org.sonar.api.web.UserRole.ISSUE_ADMIN; /** * @since 3.6 @@ -34,30 +53,163 @@ import static com.google.common.collect.Lists.newArrayList; @ServerSide public class ActionService { + private final DbClient dbClient; private final UserSession userSession; + private final Actions actions; + private final IssueService issueService; + private final IssueUpdater updater; + private final ProjectSettingsFactory projectSettingsFactory; + private final IssueStorage issueStorage; - public ActionService(UserSession userSession) { + public ActionService(DbClient dbClient, UserSession userSession, ProjectSettingsFactory projectSettingsFactory, Actions actions, IssueService issueService, IssueUpdater updater, + IssueStorage issueStorage) { + this.dbClient = dbClient; this.userSession = userSession; + this.actions = actions; + this.issueService = issueService; + this.updater = updater; + this.projectSettingsFactory = projectSettingsFactory; + this.issueStorage = issueStorage; } - public List listAvailableActions(IssueDto issue) { - List actions = newArrayList(); + public List listAvailableActions(String issueKey) { + DbSession session = dbClient.openSession(false); + try { + return listAvailableActions(issueService.getByKeyForUpdate(session, issueKey).toDefaultIssue()); + } finally { + dbClient.closeSession(session); + } + } + + public List listAvailableActions(Issue issue) { + List availableActions = newArrayList(); String login = userSession.getLogin(); if (login != null) { - actions.add("comment"); - if (issue.getResolution() == null) { - actions.add("assign"); - actions.add("set_tags"); - if (!login.equals(issue.getAssignee())) { - actions.add("assign_to_me"); + availableActions.add("comment"); + if (issue.resolution() == null) { + availableActions.add("assign"); + availableActions.add("set_tags"); + if (!login.equals(issue.assignee())) { + availableActions.add("assign_to_me"); } - actions.add("plan"); - String projectUuid = issue.getProjectUuid(); - if (projectUuid != null && userSession.hasProjectPermissionByUuid(UserRole.ISSUE_ADMIN, projectUuid)) { - actions.add("set_severity"); + availableActions.add("plan"); + String projectUuid = issue.projectUuid(); + if (projectUuid != null && userSession.hasProjectPermissionByUuid(ISSUE_ADMIN, projectUuid)) { + availableActions.add("set_severity"); } } + for (String action : loadPluginActions(issue)) { + availableActions.add(action); + } + } + return availableActions; + } + + private List loadPluginActions(final Issue issue) { + return from(actions.list()) + .filter(new SupportIssue(issue)) + .transform(ActionToKey.INSTANCE) + .toList(); + } + + public Issue execute(String issueKey, String actionKey) { + checkArgument(!Strings.isNullOrEmpty(actionKey), "Missing action"); + + DbSession session = dbClient.openSession(false); + try { + DefaultIssue issue = issueService.getByKeyForUpdate(session, issueKey).toDefaultIssue(); + Action action = getAction(actionKey, issue); + + IssueChangeContext changeContext = IssueChangeContext.createUser(new Date(), userSession.getLogin()); + FunctionContext functionContext = new FunctionContext(issue, updater, changeContext, projectSettingsFactory.newProjectSettings(issue.projectKey())); + for (Function function : action.functions()) { + function.execute(functionContext); + } + issueStorage.save(issue); + return issue; + } finally { + dbClient.closeSession(session); + } + } + + private Action getAction(String actionKey, Issue issue) { + Action action = find(actions.list(), new MatchActionKey(actionKey), null); + checkArgument(action != null, String.format("Action is not found : %s", actionKey)); + checkState(action.supports(issue), "A condition is not respected"); + return action; + } + + static class FunctionContext implements Function.Context { + + private final DefaultIssue issue; + private final IssueUpdater updater; + private final IssueChangeContext changeContext; + private final Settings projectSettings; + + FunctionContext(DefaultIssue issue, IssueUpdater updater, IssueChangeContext changeContext, Settings projectSettings) { + this.updater = updater; + this.issue = issue; + this.changeContext = changeContext; + this.projectSettings = projectSettings; + } + + @Override + public Issue issue() { + return issue; + } + + @Override + public Settings projectSettings() { + return projectSettings; + } + + @Override + public Function.Context setAttribute(String key, @Nullable String value) { + updater.setAttribute(issue, key, value, changeContext); + return this; + } + + @Override + public Function.Context addComment(@Nullable String text) { + if (text != null) { + updater.addComment(issue, text, changeContext); + } + return this; + } + } + + private static class SupportIssue implements Predicate { + private final Issue issue; + + public SupportIssue(Issue issue) { + this.issue = issue; + } + + @Override + public boolean apply(@Nonnull Action action) { + return action.supports(issue); + } + } + + private enum ActionToKey implements com.google.common.base.Function { + INSTANCE; + + @Override + public String apply(@Nonnull Action action) { + return action.key(); + } + } + + private static class MatchActionKey implements Predicate { + private final String actionKey; + + private MatchActionKey(String actionKey) { + this.actionKey = actionKey; + } + + @Override + public boolean apply(@Nonnull Action action) { + return action.key().equals(actionKey); } - return actions; } } diff --git a/server/sonar-server/src/main/java/org/sonar/server/issue/InternalRubyIssueService.java b/server/sonar-server/src/main/java/org/sonar/server/issue/InternalRubyIssueService.java index 67c580aa7e0..d8a382025db 100644 --- a/server/sonar-server/src/main/java/org/sonar/server/issue/InternalRubyIssueService.java +++ b/server/sonar-server/src/main/java/org/sonar/server/issue/InternalRubyIssueService.java @@ -82,6 +82,7 @@ public class InternalRubyIssueService { private final ResourceDao resourceDao; private final IssueFilterService issueFilterService; private final IssueBulkChangeService issueBulkChangeService; + private final ActionService actionService; private final UserSession userSession; public InternalRubyIssueService( @@ -91,7 +92,7 @@ public class InternalRubyIssueService { IssueChangelogService changelogService, ActionPlanService actionPlanService, ResourceDao resourceDao, IssueFilterService issueFilterService, IssueBulkChangeService issueBulkChangeService, - UserSession userSession) { + ActionService actionService, UserSession userSession) { this.issueService = issueService; this.issueQueryService = issueQueryService; this.commentService = commentService; @@ -100,6 +101,7 @@ public class InternalRubyIssueService { this.resourceDao = resourceDao; this.issueFilterService = issueFilterService; this.issueBulkChangeService = issueBulkChangeService; + this.actionService = actionService; this.userSession = userSession; } @@ -314,6 +316,20 @@ public class InternalRubyIssueService { return result; } + public Result executeAction(String issueKey, String actionKey) { + Result result = Result.of(); + try { + result.set(actionService.execute(issueKey, actionKey)); + } catch (Exception e) { + result.addError(e.getMessage()); + } + return result; + } + + public List listActions(String issueKey) { + return actionService.listAvailableActions(issueKey); + } + public IssueQuery emptyIssueQuery() { return issueQueryService.createFromMap(Maps.newHashMap()); } diff --git a/server/sonar-server/src/main/java/org/sonar/server/issue/ws/IssueActionsWriter.java b/server/sonar-server/src/main/java/org/sonar/server/issue/ws/IssueActionsWriter.java index 8660724f7f1..306b322f750 100644 --- a/server/sonar-server/src/main/java/org/sonar/server/issue/ws/IssueActionsWriter.java +++ b/server/sonar-server/src/main/java/org/sonar/server/issue/ws/IssueActionsWriter.java @@ -20,25 +20,24 @@ package org.sonar.server.issue.ws; -import java.util.List; import org.sonar.api.issue.Issue; import org.sonar.api.server.ServerSide; import org.sonar.api.utils.text.JsonWriter; -import org.sonar.api.web.UserRole; import org.sonar.core.issue.workflow.Transition; +import org.sonar.server.issue.ActionService; import org.sonar.server.issue.IssueService; import org.sonar.server.user.UserSession; -import static com.google.common.collect.Lists.newArrayList; - @ServerSide public class IssueActionsWriter { private final IssueService issueService; + private final ActionService actionService; private final UserSession userSession; - public IssueActionsWriter(IssueService issueService, UserSession userSession) { + public IssueActionsWriter(IssueService issueService, ActionService actionService, UserSession userSession) { this.issueService = issueService; + this.actionService = actionService; this.userSession = userSession; } @@ -54,31 +53,10 @@ public class IssueActionsWriter { public void writeActions(Issue issue, JsonWriter json) { json.name("actions").beginArray(); - for (String action : actions(issue)) { + for (String action : actionService.listAvailableActions(issue)) { json.value(action); } json.endArray(); } - private List actions(Issue issue) { - List actions = newArrayList(); - String login = userSession.getLogin(); - if (login != null) { - actions.add("comment"); - if (issue.resolution() == null) { - actions.add("assign"); - actions.add("set_tags"); - if (!login.equals(issue.assignee())) { - actions.add("assign_to_me"); - } - actions.add("plan"); - String projectUuid = issue.projectUuid(); - if (projectUuid != null && userSession.hasProjectPermissionByUuid(UserRole.ISSUE_ADMIN, projectUuid)) { - actions.add("set_severity"); - } - } - } - return actions; - } - } diff --git a/server/sonar-server/src/main/java/org/sonar/server/issue/ws/IssuesWs.java b/server/sonar-server/src/main/java/org/sonar/server/issue/ws/IssuesWs.java index 8ac665b2b7b..d5911cc2011 100644 --- a/server/sonar-server/src/main/java/org/sonar/server/issue/ws/IssuesWs.java +++ b/server/sonar-server/src/main/java/org/sonar/server/issue/ws/IssuesWs.java @@ -35,6 +35,7 @@ public class IssuesWs implements WebService { public static final String EDIT_COMMENT_ACTION = "edit_comment"; public static final String TRANSITIONS_ACTION = "transitions"; public static final String BULK_CHANGE_ACTION = "bulk_change"; + public static final String DO_ACTION_ACTION = "do_action"; private final IssuesWsAction[] actions; @@ -61,6 +62,7 @@ public class IssuesWs implements WebService { defineEditCommentAction(controller); defineTransitionsAction(controller); defineBulkChangeAction(controller); + defineDoActionAction(controller); } private static void defineChangelogAction(NewController controller) { @@ -176,4 +178,21 @@ public class IssuesWs implements WebService { RailsHandler.addFormatParam(action); } + private static void defineDoActionAction(NewController controller) { + WebService.NewAction action = controller.createAction(DO_ACTION_ACTION) + .setDescription("Do workflow transition on an issue. Requires authentication and Browse permission on project") + .setSince("3.6") + .setHandler(RailsHandler.INSTANCE) + .setPost(true); + + action.createParam("issue") + .setDescription("Key of the issue") + .setRequired(true) + .setExampleValue("5bccd6e8-f525-43a2-8d76-fcb13dde79ef"); + action.createParam("actionKey") + .setDescription("Action to perform") + .setExampleValue("link-to-jira"); + RailsHandler.addFormatParam(action); + } + } diff --git a/server/sonar-server/src/main/java/org/sonar/server/issue/ws/SearchResponseFormat.java b/server/sonar-server/src/main/java/org/sonar/server/issue/ws/SearchResponseFormat.java index ec1b2257d61..ba7ba678e19 100644 --- a/server/sonar-server/src/main/java/org/sonar/server/issue/ws/SearchResponseFormat.java +++ b/server/sonar-server/src/main/java/org/sonar/server/issue/ws/SearchResponseFormat.java @@ -138,7 +138,6 @@ public class SearchResponseFormat { if (fields.contains(SearchAdditionalField.COMMENTS)) { formatIssueComments(data, issueBuilder, dto); } - // TODO attributes result.add(issueBuilder.build()); } return result; diff --git a/server/sonar-server/src/main/java/org/sonar/server/issue/ws/SearchResponseLoader.java b/server/sonar-server/src/main/java/org/sonar/server/issue/ws/SearchResponseLoader.java index 74d8a214659..ac510fe381b 100644 --- a/server/sonar-server/src/main/java/org/sonar/server/issue/ws/SearchResponseLoader.java +++ b/server/sonar-server/src/main/java/org/sonar/server/issue/ws/SearchResponseLoader.java @@ -139,7 +139,7 @@ public class SearchResponseLoader { for (IssueDto dto : result.getIssues()) { // so that IssueDto can be used. if (collector.contains(ACTIONS)) { - result.addActions(dto.getKey(), actionService.listAvailableActions(dto)); + result.addActions(dto.getKey(), actionService.listAvailableActions(dto.toDefaultIssue())); } if (collector.contains(TRANSITIONS)) { // TODO workflow and action engines must not depend on org.sonar.api.issue.Issue but on a generic interface diff --git a/server/sonar-server/src/main/java/org/sonar/server/properties/ProjectSettings.java b/server/sonar-server/src/main/java/org/sonar/server/properties/ProjectSettings.java deleted file mode 100644 index 9eb94f93b4b..00000000000 --- a/server/sonar-server/src/main/java/org/sonar/server/properties/ProjectSettings.java +++ /dev/null @@ -1,72 +0,0 @@ -/* - * SonarQube, open source software quality management tool. - * Copyright (C) 2008-2014 SonarSource - * mailto:contact AT sonarsource DOT com - * - * SonarQube is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 3 of the License, or (at your option) any later version. - * - * SonarQube is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public License - * along with this program; if not, write to the Free Software Foundation, - * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. - */ - -package org.sonar.server.properties; - -import org.apache.commons.lang.StringUtils; -import org.sonar.api.config.Settings; - -import java.util.Map; - -public class ProjectSettings extends Settings { - private final Settings settings; - private final Map projectProperties; - - public ProjectSettings(Settings settings, Map projectProperties) { - this.settings = settings; - this.projectProperties = projectProperties; - } - - @Override - public String getString(String key) { - String value = get(key); - if (value == null) { - return settings.getString(key); - } - - return value; - } - - @Override - public boolean getBoolean(String key) { - String value = get(key); - if (value == null) { - return settings.getBoolean(key); - } - - return StringUtils.isNotEmpty(value) && Boolean.parseBoolean(value); - } - - @Override - public int getInt(String key) { - String value = get(key); - if (value == null) { - return settings.getInt(key); - } else if (StringUtils.isNotEmpty(value)) { - return Integer.parseInt(value); - } else { - return 0; - } - } - - private String get(String key) { - return projectProperties.get(key); - } -} diff --git a/server/sonar-server/src/main/java/org/sonar/server/properties/ProjectSettingsFactory.java b/server/sonar-server/src/main/java/org/sonar/server/properties/ProjectSettingsFactory.java index 43986c1ea35..994c3377aaf 100644 --- a/server/sonar-server/src/main/java/org/sonar/server/properties/ProjectSettingsFactory.java +++ b/server/sonar-server/src/main/java/org/sonar/server/properties/ProjectSettingsFactory.java @@ -20,16 +20,12 @@ package org.sonar.server.properties; -import com.google.common.annotations.VisibleForTesting; -import com.google.common.collect.Maps; +import java.util.List; import org.sonar.api.config.Settings; import org.sonar.api.server.ServerSide; import org.sonar.db.property.PropertiesDao; import org.sonar.db.property.PropertyDto; -import java.util.List; -import java.util.Map; - @ServerSide public class ProjectSettingsFactory { @@ -43,19 +39,10 @@ public class ProjectSettingsFactory { public Settings newProjectSettings(String projectKey) { List propertyList = dao.selectProjectProperties(projectKey); - - return new ProjectSettings(settings, getPropertyMap(propertyList)); - } - - @VisibleForTesting - Map getPropertyMap(List propertyDtoList) { - Map propertyMap = Maps.newHashMap(); - for (PropertyDto property : propertyDtoList) { - String key = property.getKey(); - String value = property.getValue(); - propertyMap.put(key, value); + Settings projectSettings = new Settings(settings); + for (PropertyDto property : propertyList) { + projectSettings.setProperty(property.getKey(), property.getValue()); } - - return propertyMap; + return projectSettings; } } diff --git a/server/sonar-server/src/test/java/org/sonar/server/issue/ActionServiceTest.java b/server/sonar-server/src/test/java/org/sonar/server/issue/ActionServiceTest.java new file mode 100644 index 00000000000..0d6570bcfc3 --- /dev/null +++ b/server/sonar-server/src/test/java/org/sonar/server/issue/ActionServiceTest.java @@ -0,0 +1,238 @@ +/* + * SonarQube, open source software quality management tool. + * Copyright (C) 2008-2014 SonarSource + * mailto:contact AT sonarsource DOT com + * + * SonarQube is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * SonarQube is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +package org.sonar.server.issue; + +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.ExpectedException; +import org.sonar.api.config.Settings; +import org.sonar.api.issue.Issue; +import org.sonar.api.issue.action.Actions; +import org.sonar.api.issue.action.Function; +import org.sonar.api.issue.condition.Condition; +import org.sonar.core.issue.DefaultIssue; +import org.sonar.core.issue.IssueChangeContext; +import org.sonar.core.issue.IssueUpdater; +import org.sonar.db.DbClient; +import org.sonar.db.DbSession; +import org.sonar.db.component.ComponentDto; +import org.sonar.db.issue.IssueDto; +import org.sonar.server.properties.ProjectSettingsFactory; +import org.sonar.server.tester.UserSessionRule; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Matchers.any; +import static org.mockito.Matchers.eq; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyNoMoreInteractions; +import static org.mockito.Mockito.when; +import static org.sonar.api.issue.Issue.RESOLUTION_FIXED; +import static org.sonar.api.web.UserRole.ISSUE_ADMIN; +import static org.sonar.db.component.ComponentTesting.newFileDto; +import static org.sonar.db.component.ComponentTesting.newProjectDto; +import static org.sonar.db.rule.RuleTesting.newXooX1; + +public class ActionServiceTest { + + static final String PROJECT_KEY = "PROJECT_KEY"; + static final String PROJECT_UUID = "PROJECT_UUID"; + + static final String ISSUE_KEY = "ISSUE_KEY"; + static final String PLUGIN_ACTION = "link-to-jira"; + + @Rule + public ExpectedException thrown = ExpectedException.none(); + + @Rule + public UserSessionRule userSession = UserSessionRule.standalone().login("arthur"); + + DbClient dbClient = mock(DbClient.class); + DbSession session = mock(DbSession.class); + + IssueService issueService = mock(IssueService.class); + IssueStorage issueStorage = mock(IssueStorage.class); + IssueUpdater updater = mock(IssueUpdater.class); + ProjectSettingsFactory projectSettingsFactory = mock(ProjectSettingsFactory.class); + Settings settings = new Settings(); + Actions actions = new Actions(); + ActionService actionService; + + ComponentDto project; + IssueDto issue; + + @Before + public void before() { + when(dbClient.openSession(false)).thenReturn(session); + when(projectSettingsFactory.newProjectSettings(PROJECT_KEY)).thenReturn(settings); + + project = newProjectDto(PROJECT_UUID).setKey(PROJECT_KEY); + issue = IssueTesting.newDto(newXooX1().setId(10), newFileDto(project), project).setKee(ISSUE_KEY); + + actionService = new ActionService(dbClient, userSession, projectSettingsFactory, actions, issueService, updater, issueStorage); + } + + @Test + public void execute_functions() { + Function function1 = mock(Function.class); + Function function2 = mock(Function.class); + + when(issueService.getByKeyForUpdate(session, ISSUE_KEY)).thenReturn(issue); + + actions.add(PLUGIN_ACTION).setConditions(new AlwaysMatch()).setFunctions(function1, function2); + + assertThat(actionService.execute(ISSUE_KEY, PLUGIN_ACTION)).isNotNull(); + + verify(function1).execute(any(Function.Context.class)); + verify(function2).execute(any(Function.Context.class)); + verifyNoMoreInteractions(function1, function2); + } + + @Test + public void modify_issue_when_executing_a_function() { + Function function = new TweetFunction(); + when(issueService.getByKeyForUpdate(session, ISSUE_KEY)).thenReturn(issue); + + actions.add(PLUGIN_ACTION).setConditions(new AlwaysMatch()).setFunctions(function); + assertThat(actionService.execute(ISSUE_KEY, PLUGIN_ACTION)).isNotNull(); + + verify(updater).addComment(any(DefaultIssue.class), eq("New tweet on issue ISSUE_KEY"), any(IssueChangeContext.class)); + verify(updater).setAttribute(any(DefaultIssue.class), eq("tweet"), eq("tweet sent"), any(IssueChangeContext.class)); + } + + @Test + public void use_project_settings_when_executing_a_function() { + Function function = new SettingsFunction(); + when(issueService.getByKeyForUpdate(session, ISSUE_KEY)).thenReturn(issue); + settings.setProperty("key", "value"); + + actions.add(PLUGIN_ACTION).setConditions(new AlwaysMatch()).setFunctions(function); + actionService.execute(ISSUE_KEY, PLUGIN_ACTION); + + verify(updater).addComment(any(DefaultIssue.class), eq("Property 'key' is 'value'"), any(IssueChangeContext.class)); + } + + @Test + public void not_execute_function_if_action_not_found() { + thrown.expect(IllegalArgumentException.class); + thrown.expectMessage("Action is not found : tweet"); + + Function function = mock(Function.class); + when(issueService.getByKeyForUpdate(session, ISSUE_KEY)).thenReturn(issue); + actions.add(PLUGIN_ACTION).setConditions(new AlwaysMatch()).setFunctions(function); + actionService.execute(ISSUE_KEY, "tweet"); + } + + @Test + public void not_execute_function_if_action_is_not_supported() { + thrown.expect(IllegalStateException.class); + thrown.expectMessage("A condition is not respected"); + + Function function = mock(Function.class); + + when(issueService.getByKeyForUpdate(session, ISSUE_KEY)).thenReturn(issue); + actions.add(PLUGIN_ACTION).setConditions(new NeverMatch()).setFunctions(function); + actionService.execute(ISSUE_KEY, PLUGIN_ACTION); + } + + @Test + public void return_plugin_actions() { + actions.add(PLUGIN_ACTION).setConditions(new AlwaysMatch()); + actions.add("tweet").setConditions(new NeverMatch()); + assertThat(actionService.listAvailableActions(issue.toDefaultIssue())).contains(PLUGIN_ACTION); + } + + @Test + public void return_plugin_actions_on_resolved_issue() { + actions.add(PLUGIN_ACTION).setConditions(new AlwaysMatch()); + actions.add("tweet").setConditions(new NeverMatch()); + issue = IssueTesting.newDto(newXooX1().setId(10), newFileDto(project), project).setKee(ISSUE_KEY).setResolution(RESOLUTION_FIXED); + + assertThat(actionService.listAvailableActions(issue.toDefaultIssue())).contains(PLUGIN_ACTION); + } + + @Test + public void return_provided_actions_without_set_severity_when_not_issue_admin() { + assertThat(actionService.listAvailableActions(issue.toDefaultIssue())).containsOnly("comment", "assign", "set_tags", "assign_to_me", "plan"); + } + + @Test + public void return_provided_actions_with_set_severity_when_issue_admin() { + userSession.addProjectUuidPermissions(ISSUE_ADMIN, PROJECT_UUID); + assertThat(actionService.listAvailableActions(issue.toDefaultIssue())).containsOnly("comment", "assign", "set_tags", "assign_to_me", "plan", "set_severity"); + } + + @Test + public void return_no_actions_when_not_logged() { + userSession.anonymous(); + assertThat(actionService.listAvailableActions(issue.toDefaultIssue())).isEmpty(); + } + + @Test + public void doest_not_return_assign_to_me_action_when_issue_already_assigned_to_user() { + userSession.login("julien"); + IssueDto issue = IssueTesting.newDto(newXooX1().setId(10), newFileDto(project), project).setKee(ISSUE_KEY).setAssignee("julien"); + assertThat(actionService.listAvailableActions(issue.toDefaultIssue())).doesNotContain("assign_to_me"); + } + + @Test + public void return_only_comment_action_when_issue_has_a_resolution() { + IssueDto issue = IssueTesting.newDto(newXooX1().setId(10), newFileDto(project), project).setKee(ISSUE_KEY).setResolution(RESOLUTION_FIXED); + assertThat(actionService.listAvailableActions(issue.toDefaultIssue())).containsOnly("comment"); + } + + @Test + public void return_actions_by_issue_key() { + when(issueService.getByKeyForUpdate(session, ISSUE_KEY)).thenReturn(issue); + assertThat(actionService.listAvailableActions(ISSUE_KEY)).isNotEmpty(); + } + + public class AlwaysMatch implements Condition { + @Override + public boolean matches(Issue issue) { + return true; + } + } + + public class NeverMatch implements Condition { + @Override + public boolean matches(Issue issue) { + return false; + } + } + + public class TweetFunction implements Function { + @Override + public void execute(Context context) { + context.addComment("New tweet on issue " + context.issue().key()); + context.setAttribute("tweet", "tweet sent"); + } + } + + public class SettingsFunction implements Function { + @Override + public void execute(Context context) { + context.addComment(String.format("Property 'key' is '%s'", context.projectSettings().getString("key"))); + } + } + +} diff --git a/server/sonar-server/src/test/java/org/sonar/server/issue/InternalRubyIssueServiceTest.java b/server/sonar-server/src/test/java/org/sonar/server/issue/InternalRubyIssueServiceTest.java index b184a828279..345cc039249 100644 --- a/server/sonar-server/src/test/java/org/sonar/server/issue/InternalRubyIssueServiceTest.java +++ b/server/sonar-server/src/test/java/org/sonar/server/issue/InternalRubyIssueServiceTest.java @@ -82,6 +82,8 @@ public class InternalRubyIssueServiceTest { IssueBulkChangeService issueBulkChangeService; + ActionService actionService; + InternalRubyIssueService service; @Before @@ -94,12 +96,13 @@ public class InternalRubyIssueServiceTest { resourceDao = mock(ResourceDao.class); issueFilterService = mock(IssueFilterService.class); issueBulkChangeService = mock(IssueBulkChangeService.class); + actionService = mock(ActionService.class); ResourceDto project = new ResourceDto().setKey("org.sonar.Sample"); when(resourceDao.selectResource(any(ResourceQuery.class))).thenReturn(project); service = new InternalRubyIssueService(issueService, issueQueryService, commentService, changelogService, actionPlanService, resourceDao, - issueFilterService, issueBulkChangeService, userSessionRule); + issueFilterService, issueBulkChangeService, actionService, userSessionRule); } @Test diff --git a/server/sonar-server/src/test/java/org/sonar/server/issue/ws/IssueActionsWriterTest.java b/server/sonar-server/src/test/java/org/sonar/server/issue/ws/IssueActionsWriterTest.java index 050ce927fe1..7232fb5b8a5 100644 --- a/server/sonar-server/src/test/java/org/sonar/server/issue/ws/IssueActionsWriterTest.java +++ b/server/sonar-server/src/test/java/org/sonar/server/issue/ws/IssueActionsWriterTest.java @@ -30,9 +30,9 @@ import org.mockito.runners.MockitoJUnitRunner; import org.sonar.api.issue.Issue; import org.sonar.api.rule.RuleKey; import org.sonar.api.utils.text.JsonWriter; -import org.sonar.api.web.UserRole; import org.sonar.core.issue.DefaultIssue; import org.sonar.core.issue.workflow.Transition; +import org.sonar.server.issue.ActionService; import org.sonar.server.issue.IssueService; import org.sonar.server.tester.UserSessionRule; import org.sonar.test.JsonAssert; @@ -49,107 +49,74 @@ public class IssueActionsWriterTest { @Mock IssueService issueService; + @Mock + ActionService actionService; + IssueActionsWriter writer; @Before public void setUp() { - writer = new IssueActionsWriter(issueService, userSessionRule); - } - - @Test - public void write_all_standard_actions() { - Issue issue = new DefaultIssue() - .setKey("ABCD") - .setComponentUuid("BCDE") - .setComponentKey("sample:src/main/xoo/sample/Sample.xoo") - .setProjectUuid("ABCD") - .setProjectKey("sample") - .setRuleKey(RuleKey.of("squid", "AvoidCycle")); - - userSessionRule.login("john").addProjectUuidPermissions(UserRole.ISSUE_ADMIN, "ABCD"); - - testActions(issue, - "{\"actions\": " + - "[" + - "\"comment\", \"assign\", \"set_tags\", \"assign_to_me\", \"plan\", \"set_severity\"\n" + - "]}"); + writer = new IssueActionsWriter(issueService, actionService, userSessionRule); } @Test - public void write_only_comment_action() { + public void write_actions() { Issue issue = new DefaultIssue() .setKey("ABCD") .setComponentKey("sample:src/main/xoo/sample/Sample.xoo") .setProjectKey("sample") .setRuleKey(RuleKey.of("squid", "AvoidCycle")) - .setResolution("CLOSED"); + .setAssignee("john"); + when(actionService.listAvailableActions(issue)).thenReturn(newArrayList("comment")); userSessionRule.login("john"); testActions(issue, "{\"actions\": " + "[" + - "\"comment\"" + + "\"comment\"\n" + "]}"); } @Test - public void write_no_action_if_not_logged() { + public void write_transitions() { Issue issue = new DefaultIssue() .setKey("ABCD") .setComponentKey("sample:src/main/xoo/sample/Sample.xoo") .setProjectKey("sample") .setRuleKey(RuleKey.of("squid", "AvoidCycle")); - testActions(issue, - "{\"actions\": []}"); - } - - @Test - public void write_actions_without_assign_to_me() { - Issue issue = new DefaultIssue() - .setKey("ABCD") - .setComponentKey("sample:src/main/xoo/sample/Sample.xoo") - .setProjectKey("sample") - .setRuleKey(RuleKey.of("squid", "AvoidCycle")) - .setAssignee("john"); - + when(issueService.listTransitions(eq(issue))).thenReturn(newArrayList(Transition.create("reopen", "RESOLVED", "REOPEN"))); userSessionRule.login("john"); - testActions(issue, - "{\"actions\": " + - "[" + - "\"comment\", \"assign\", \"set_tags\", \"plan\"\n" + - "]}"); + testTransitions(issue, + "{\"transitions\": [\n" + + " \"reopen\"\n" + + " ]}"); } @Test - public void write_transitions() { + public void write_no_transitions() { Issue issue = new DefaultIssue() .setKey("ABCD") .setComponentKey("sample:src/main/xoo/sample/Sample.xoo") .setProjectKey("sample") .setRuleKey(RuleKey.of("squid", "AvoidCycle")); - when(issueService.listTransitions(eq(issue))).thenReturn(newArrayList(Transition.create("reopen", "RESOLVED", "REOPEN"))); userSessionRule.login("john"); testTransitions(issue, - "{\"transitions\": [\n" + - " \"reopen\"\n" + - " ]}"); + "{\"transitions\": []}"); } @Test - public void write_no_transitions() { + public void write_no_transitions_if_not_logged() { Issue issue = new DefaultIssue() .setKey("ABCD") .setComponentKey("sample:src/main/xoo/sample/Sample.xoo") .setProjectKey("sample") .setRuleKey(RuleKey.of("squid", "AvoidCycle")); - userSessionRule.login("john"); - testTransitions(issue, "{\"transitions\": []}"); } diff --git a/server/sonar-server/src/test/java/org/sonar/server/issue/ws/SearchActionMediumTest.java b/server/sonar-server/src/test/java/org/sonar/server/issue/ws/SearchActionMediumTest.java index df93f08dd4a..c592d1f993b 100644 --- a/server/sonar-server/src/test/java/org/sonar/server/issue/ws/SearchActionMediumTest.java +++ b/server/sonar-server/src/test/java/org/sonar/server/issue/ws/SearchActionMediumTest.java @@ -20,11 +20,9 @@ package org.sonar.server.issue.ws; -import com.google.common.collect.ImmutableMap; import org.junit.After; import org.junit.Before; import org.junit.ClassRule; -import org.junit.Ignore; import org.junit.Rule; import org.junit.Test; import org.sonar.api.issue.Issue; @@ -32,12 +30,12 @@ import org.sonar.api.rule.RuleStatus; import org.sonar.api.security.DefaultGroups; import org.sonar.api.server.ws.WebService; import org.sonar.api.utils.DateUtils; -import org.sonar.api.utils.KeyValueFormat; import org.sonar.api.web.UserRole; import org.sonar.core.permission.GlobalPermissions; import org.sonar.db.DbClient; import org.sonar.db.DbSession; import org.sonar.db.component.ComponentDto; +import org.sonar.db.component.ComponentTesting; import org.sonar.db.issue.ActionPlanDao; import org.sonar.db.issue.ActionPlanDto; import org.sonar.db.issue.IssueChangeDao; @@ -47,7 +45,6 @@ import org.sonar.db.issue.IssueDto; import org.sonar.db.rule.RuleDto; import org.sonar.db.rule.RuleTesting; import org.sonar.db.user.UserDto; -import org.sonar.db.component.ComponentTesting; import org.sonar.server.issue.IssueQuery; import org.sonar.server.issue.IssueTesting; import org.sonar.server.issue.filter.IssueFilterParameters; @@ -241,24 +238,6 @@ public class SearchActionMediumTest { result.assertJson(this.getClass(), "issue_with_action_plan.json"); } - @Ignore("temporarily disabled") - @Test - public void issue_with_attributes() throws Exception { - ComponentDto project = insertComponent(ComponentTesting.newProjectDto("PROJECT_ID").setKey("PROJECT_KEY")); - setDefaultProjectPermission(project); - ComponentDto file = insertComponent(ComponentTesting.newFileDto(project, "FILE_ID").setKey("FILE_KEY")); - IssueDto issue = IssueTesting.newDto(newRule(), file, project) - .setKee("82fd47d4-b650-4037-80bc-7b112bd4eac2") - .setIssueAttributes(KeyValueFormat.format(ImmutableMap.of("jira-issue-key", "SONAR-1234"))); - db.issueDao().insert(session, issue); - session.commit(); - tester.get(IssueIndexer.class).indexAll(); - - WsTester.Result result = wsTester.newGetRequest(IssuesWs.API_ENDPOINT, SearchAction.SEARCH_ACTION) - .execute(); - result.assertJson(this.getClass(), "issue_with_attributes.json"); - } - @Test public void load_additional_fields() throws Exception { db.userDao().insert(session, new UserDto().setLogin("simon").setName("Simon").setEmail("simon@email.com")); diff --git a/server/sonar-server/src/test/java/org/sonar/server/issue/ws/ShowActionTest.java b/server/sonar-server/src/test/java/org/sonar/server/issue/ws/ShowActionTest.java index 661ab46de43..5a8aefe66a9 100644 --- a/server/sonar-server/src/test/java/org/sonar/server/issue/ws/ShowActionTest.java +++ b/server/sonar-server/src/test/java/org/sonar/server/issue/ws/ShowActionTest.java @@ -51,6 +51,7 @@ import org.sonar.db.component.ComponentDao; import org.sonar.db.component.ComponentDto; import org.sonar.db.component.ComponentTesting; import org.sonar.server.debt.DebtModelService; +import org.sonar.server.issue.ActionService; import org.sonar.server.issue.IssueChangelog; import org.sonar.server.issue.IssueChangelogService; import org.sonar.server.issue.IssueCommentService; @@ -90,6 +91,9 @@ public class ShowActionTest { @Mock IssueService issueService; + @Mock + ActionService actionService; + @Mock IssueChangelogService issueChangelogService; @@ -142,7 +146,7 @@ public class ShowActionTest { tester = new WsTester(new IssuesWs( new ShowAction( dbClient, issueService, issueChangelogService, commentService, - new IssueActionsWriter(issueService, userSessionRule), + new IssueActionsWriter(issueService, actionService, userSessionRule), actionPlanService, userFinder, debtModel, ruleService, i18n, durations, userSessionRule))); } @@ -455,6 +459,8 @@ public class ShowActionTest { .setStatus("OPEN"); when(issueService.getByKey(issue.key())).thenReturn(issue); + when(actionService.listAvailableActions(issue)).thenReturn(newArrayList( "comment", "assign", "set_tags", "assign_to_me", "plan")); + userSessionRule.login("john"); WsTester.TestRequest request = tester.newGetRequest("api/issues", "show").setParam("key", issue.key()); request.execute().assertJson(getClass(), "show_issue_with_actions.json"); diff --git a/server/sonar-server/src/test/java/org/sonar/server/properties/ProjectSettingsFactoryTest.java b/server/sonar-server/src/test/java/org/sonar/server/properties/ProjectSettingsFactoryTest.java new file mode 100644 index 00000000000..8ceff5e1b68 --- /dev/null +++ b/server/sonar-server/src/test/java/org/sonar/server/properties/ProjectSettingsFactoryTest.java @@ -0,0 +1,76 @@ +/* + * SonarQube, open source software quality management tool. + * Copyright (C) 2008-2014 SonarSource + * mailto:contact AT sonarsource DOT com + * + * SonarQube is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * SonarQube is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +package org.sonar.server.properties; + +import org.junit.Test; +import org.sonar.api.config.Settings; +import org.sonar.db.property.PropertiesDao; +import org.sonar.db.property.PropertyDto; + +import static com.google.common.collect.Lists.newArrayList; +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +public class ProjectSettingsFactoryTest { + + static final String PROJECT_KEY = "PROJECT_KEY"; + + Settings settings = new Settings(); + PropertiesDao dao = mock(PropertiesDao.class); + + ProjectSettingsFactory underTest = new ProjectSettingsFactory(settings, dao); + + @Test + public void return_global_settings() { + settings.setProperty("key", "value"); + Settings projectSettings = underTest.newProjectSettings(PROJECT_KEY); + + assertThat(projectSettings.getProperties()).hasSize(1); + assertThat(projectSettings.getString("key")).isEqualTo("value"); + } + + @Test + public void return_project_settings() { + when(dao.selectProjectProperties(PROJECT_KEY)).thenReturn(newArrayList( + new PropertyDto().setKey("1").setValue("val1"), + new PropertyDto().setKey("2").setValue("val2"), + new PropertyDto().setKey("3").setValue("val3")) + ); + + Settings projectSettings = underTest.newProjectSettings(PROJECT_KEY); + + assertThat(projectSettings.getString("1")).isEqualTo("val1"); + assertThat(projectSettings.getString("2")).isEqualTo("val2"); + assertThat(projectSettings.getString("3")).isEqualTo("val3"); + } + + @Test + public void project_settings_override_global_settings() { + settings.setProperty("key", "value"); + when(dao.selectProjectProperties(PROJECT_KEY)).thenReturn(newArrayList( + new PropertyDto().setKey("key").setValue("value2")) + ); + + Settings projectSettings = underTest.newProjectSettings(PROJECT_KEY); + assertThat(projectSettings.getString("key")).isEqualTo("value2"); + } +} diff --git a/server/sonar-server/src/test/java/org/sonar/server/properties/ProjectSettingsRespositoryFactoryTest.java b/server/sonar-server/src/test/java/org/sonar/server/properties/ProjectSettingsRespositoryFactoryTest.java deleted file mode 100644 index 59ed79f206e..00000000000 --- a/server/sonar-server/src/test/java/org/sonar/server/properties/ProjectSettingsRespositoryFactoryTest.java +++ /dev/null @@ -1,74 +0,0 @@ -/* - * SonarQube, open source software quality management tool. - * Copyright (C) 2008-2014 SonarSource - * mailto:contact AT sonarsource DOT com - * - * SonarQube is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 3 of the License, or (at your option) any later version. - * - * SonarQube is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public License - * along with this program; if not, write to the Free Software Foundation, - * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. - */ - -package org.sonar.server.properties; - -import com.google.common.collect.Lists; -import org.junit.Before; -import org.junit.Test; -import org.sonar.api.config.Settings; -import org.sonar.db.property.PropertiesDao; -import org.sonar.db.property.PropertyDto; - -import java.util.Map; - -import static com.google.common.collect.Lists.newArrayList; -import static org.assertj.core.api.Assertions.assertThat; -import static org.mockito.Mockito.mock; - -public class ProjectSettingsRespositoryFactoryTest { - - private ProjectSettingsFactory underTest; - - @Before - public void before() { - Settings settings = mock(Settings.class); - PropertiesDao dao = mock(PropertiesDao.class); - - this.underTest = new ProjectSettingsFactory(settings, dao); - } - - @Test - public void newProjectSettings_returns_a_ProjectSettings() { - Settings projectSettings = underTest.newProjectSettings("PROJECT_KEY"); - - assertThat(projectSettings).isInstanceOf(ProjectSettings.class); - } - - @Test - public void transform_empty_list_into_empty_map() { - Map propertyMap = underTest.getPropertyMap(Lists.newArrayList()); - - assertThat(propertyMap).isEmpty(); - } - - @Test - public void transform_list_of_properties_in_map_key_value() { - PropertyDto property1 = new PropertyDto().setKey("1").setValue("val1"); - PropertyDto property2 = new PropertyDto().setKey("2").setValue("val2"); - PropertyDto property3 = new PropertyDto().setKey("3").setValue("val3"); - - Map propertyMap = underTest.getPropertyMap(newArrayList(property1, property2, property3)); - - assertThat(propertyMap.get("1")).isEqualTo("val1"); - assertThat(propertyMap.get("2")).isEqualTo("val2"); - assertThat(propertyMap.get("3")).isEqualTo("val3"); - } -} diff --git a/server/sonar-server/src/test/java/org/sonar/server/properties/ProjectSettingsRespositoryTest.java b/server/sonar-server/src/test/java/org/sonar/server/properties/ProjectSettingsRespositoryTest.java deleted file mode 100644 index 6ea1e586b69..00000000000 --- a/server/sonar-server/src/test/java/org/sonar/server/properties/ProjectSettingsRespositoryTest.java +++ /dev/null @@ -1,137 +0,0 @@ -/* - * SonarQube, open source software quality management tool. - * Copyright (C) 2008-2014 SonarSource - * mailto:contact AT sonarsource DOT com - * - * SonarQube is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 3 of the License, or (at your option) any later version. - * - * SonarQube is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public License - * along with this program; if not, write to the Free Software Foundation, - * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. - */ - -package org.sonar.server.properties; - -import com.google.common.collect.Maps; -import org.junit.Before; -import org.junit.Test; -import org.sonar.api.config.Settings; - -import java.util.HashMap; - -import static org.assertj.core.api.Assertions.assertThat; -import static org.mockito.Mockito.anyString; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.times; -import static org.mockito.Mockito.verify; - -public class ProjectSettingsRespositoryTest { - - private ProjectSettings underTest; - - private Settings settings; - - @Before - public void before() { - this.settings = mock(Settings.class); - } - - @Test - public void call_global_settings_method_when_no_project_specific_settings() { - this.underTest = new ProjectSettings(settings, Maps.newHashMap()); - - underTest.getInt("anyKey"); - underTest.getBoolean("anyKey"); - underTest.getString("anyKey"); - - verify(settings, times(1)).getBoolean(anyString()); - verify(settings, times(1)).getInt(anyString()); - verify(settings, times(1)).getString(anyString()); - } - - @Test(expected = NumberFormatException.class) - public void getInt_property_throws_exception_when_value_is_not_formatted_correctly() { - HashMap properties = Maps.newHashMap(); - properties.put("intKey", "wrongIntValue"); - this.underTest = new ProjectSettings(settings, properties); - - underTest.getInt("intKey"); - } - - @Test - public void getInt_property_return_0_when_empty_property() { - HashMap properties = Maps.newHashMap(); - properties.put("intKey", ""); - this.underTest = new ProjectSettings(settings, properties); - - int value = underTest.getInt("intKey"); - - assertThat(value).isEqualTo(0); - } - - @Test - public void getInt_property_return_the_int_value() { - HashMap properties = Maps.newHashMap(); - properties.put("intKey", "123"); - this.underTest = new ProjectSettings(settings, properties); - - int value = underTest.getInt("intKey"); - - assertThat(value).isEqualTo(123); - } - - @Test - public void getString_returns_String_property() { - HashMap properties = Maps.newHashMap(); - properties.put("stringKey", "stringValue"); - this.underTest = new ProjectSettings(settings, properties); - - String value = underTest.getString("stringKey"); - - assertThat(value).isEqualTo("stringValue"); - } - - @Test - public void getBoolean_returns_exception_when_value_is_not_formatted_correctly() { - HashMap properties = Maps.newHashMap(); - properties.put("boolKey", "wronglyFormattedBoolean"); - this.underTest = new ProjectSettings(settings, properties); - - boolean key = underTest.getBoolean("boolKey"); - - assertThat(key).isFalse(); - } - - @Test - public void getBoolean_returns_false_when_value_is_empty() { - HashMap properties = Maps.newHashMap(); - properties.put("boolKey", ""); - this.underTest = new ProjectSettings(settings, properties); - - boolean key = underTest.getBoolean("boolKey"); - - assertThat(key).isFalse(); - } - - @Test - public void getBoolean_returns_true_when_value_is_true_ignoring_case() { - HashMap properties = Maps.newHashMap(); - properties.put("boolKey1", "true"); - properties.put("boolKey2", "True"); - this.underTest = new ProjectSettings(settings, properties); - - boolean key1 = underTest.getBoolean("boolKey1"); - boolean key2 = underTest.getBoolean("boolKey2"); - - assertThat(key1).isTrue(); - assertThat(key2).isTrue(); - } -} diff --git a/server/sonar-server/src/test/resources/org/sonar/server/issue/ws/ShowActionTest/show_issue_with_comments.json b/server/sonar-server/src/test/resources/org/sonar/server/issue/ws/ShowActionTest/show_issue_with_comments.json index 0827d1f204e..8c143219aa2 100644 --- a/server/sonar-server/src/test/resources/org/sonar/server/issue/ws/ShowActionTest/show_issue_with_comments.json +++ b/server/sonar-server/src/test/resources/org/sonar/server/issue/ws/ShowActionTest/show_issue_with_comments.json @@ -12,7 +12,6 @@ "creationDate": "2014-01-22T19:10:03+0100", "fCreationDate": "Jan 22, 2014 10:03 AM", "transitions": [], - "actions": ["comment", "assign", "set_tags", "assign_to_me", "plan"], "comments": [ { "key": "COMMENT-ABCD", diff --git a/server/sonar-server/src/test/resources/org/sonar/server/issue/ws/ShowActionTest/show_issue_with_transitions.json b/server/sonar-server/src/test/resources/org/sonar/server/issue/ws/ShowActionTest/show_issue_with_transitions.json index 9d06cd9d942..fb2e1b2355f 100644 --- a/server/sonar-server/src/test/resources/org/sonar/server/issue/ws/ShowActionTest/show_issue_with_transitions.json +++ b/server/sonar-server/src/test/resources/org/sonar/server/issue/ws/ShowActionTest/show_issue_with_transitions.json @@ -14,7 +14,6 @@ "creationDate": "2014-01-22T19:10:03+0100", "fCreationDate": "Jan 22, 2014 10:03 AM", "transitions": ["reopen"], - "actions": ["comment"], "comments": [], "changelog": [ { diff --git a/server/sonar-web/src/main/webapp/WEB-INF/app/controllers/api/issues_controller.rb b/server/sonar-web/src/main/webapp/WEB-INF/app/controllers/api/issues_controller.rb index deb21bae193..e6488d26abc 100644 --- a/server/sonar-web/src/main/webapp/WEB-INF/app/controllers/api/issues_controller.rb +++ b/server/sonar-web/src/main/webapp/WEB-INF/app/controllers/api/issues_controller.rb @@ -140,14 +140,35 @@ class Api::IssuesController < Api::ApiController def actions require_parameters :issue issue_key = params[:issue] - actions = Internal.issues.listActions(issue_key) render :json => jsonp( { - :actions => actions.map { |t| t.key() } + :actions => Internal.issues.listActions(issue_key) } ) end + # + # POST /api/issues/do_action?issue=&actionKey= + # + # -- Example + # curl -X POST -v -u admin:admin 'http://localhost:9000/api/issues/do_action?issue=9b6f89c0-3347-46f6-a6d1-dd6c761240e0&actionKey=link-to-jira' + # + def do_action + verify_post_request + require_parameters :issue, :actionKey + + result = Internal.issues.executeAction(params[:issue], params[:actionKey]) + + http_status = (result.ok ? 200 : 400) + hash = result_to_hash(result) + + respond_to do |format| + # if the request header "Accept" is "*/*", then the default format is the first one (json) + format.json { render :json => jsonp(hash), :status => result.httpStatus } + format.xml { render :xml => hash.to_xml(:skip_types => true, :root => 'sonar', :status => http_status) } + end + end + # # Execute a bulk change on a list of issues # diff --git a/sonar-plugin-api/src/main/java/org/sonar/api/issue/action/Action.java b/sonar-plugin-api/src/main/java/org/sonar/api/issue/action/Action.java index 4a7254cfabe..46f6dabbf4e 100644 --- a/sonar-plugin-api/src/main/java/org/sonar/api/issue/action/Action.java +++ b/sonar-plugin-api/src/main/java/org/sonar/api/issue/action/Action.java @@ -30,9 +30,7 @@ import static com.google.common.collect.Lists.newArrayList; /** * @since 3.6 - * @deprecated in 5.2. Webapp can not be customized anymore to define actions on issues. */ -@Deprecated public class Action { private final String key; diff --git a/sonar-plugin-api/src/main/java/org/sonar/api/issue/action/Actions.java b/sonar-plugin-api/src/main/java/org/sonar/api/issue/action/Actions.java index 3386cd33cf9..813a3e90990 100644 --- a/sonar-plugin-api/src/main/java/org/sonar/api/issue/action/Actions.java +++ b/sonar-plugin-api/src/main/java/org/sonar/api/issue/action/Actions.java @@ -25,22 +25,20 @@ import org.sonar.api.server.ServerSide; /** * @since 3.6 - * @deprecated in 5.2. Webapp can not be customized anymore to define actions on issues. */ -@Deprecated @ServerSide public class Actions { - private final List actions = new ArrayList<>(); + private final List list = new ArrayList<>(); public Action add(String actionKey) { Action action = new Action(actionKey); - this.actions.add(action); + this.list.add(action); return action; } public List list() { - return actions; + return list; } }