]> source.dussan.org Git - sonarqube.git/commitdiff
SONAR-5900 Add api/issues/set_tags WS
authorJean-Baptiste Lievremont <jean-baptiste.lievremont@sonarsource.com>
Thu, 11 Dec 2014 10:12:56 +0000 (11:12 +0100)
committerJean-Baptiste Lievremont <jean-baptiste.lievremont@sonarsource.com>
Thu, 11 Dec 2014 15:33:45 +0000 (16:33 +0100)
15 files changed:
server/sonar-server/src/main/java/org/sonar/server/issue/IssueService.java
server/sonar-server/src/main/java/org/sonar/server/issue/ws/IssuesWs.java
server/sonar-server/src/main/java/org/sonar/server/issue/ws/SetTagsAction.java [new file with mode: 0644]
server/sonar-server/src/main/java/org/sonar/server/platform/ServerComponents.java
server/sonar-server/src/test/java/org/sonar/server/issue/IssueServiceMediumTest.java
server/sonar-server/src/test/java/org/sonar/server/issue/ws/IssueShowActionTest.java
server/sonar-server/src/test/java/org/sonar/server/issue/ws/IssueTagsActionTest.java
server/sonar-server/src/test/java/org/sonar/server/issue/ws/IssuesWsTest.java
server/sonar-server/src/test/java/org/sonar/server/issue/ws/SetTagsActionTest.java [new file with mode: 0644]
sonar-core/src/main/java/org/sonar/core/issue/IssueUpdater.java
sonar-core/src/main/java/org/sonar/core/issue/db/IssueDto.java
sonar-core/src/main/resources/org/sonar/core/issue/db/IssueMapper.xml
sonar-core/src/main/resources/org/sonar/l10n/core.properties
sonar-plugin-api/src/main/java/org/sonar/api/issue/Issue.java
sonar-plugin-api/src/main/java/org/sonar/api/issue/internal/DefaultIssue.java

index c742a45afda9b8dbabe2ebf8c8df391e4e7f481e..83d0785d475e8e2db208297fbf9528bab241186e 100644 (file)
@@ -59,7 +59,12 @@ import org.sonar.server.user.UserSession;
 
 import javax.annotation.Nullable;
 
-import java.util.*;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.Date;
+import java.util.List;
+import java.util.Map;
 
 import static com.google.common.collect.Maps.newLinkedHashMap;
 
@@ -348,4 +353,21 @@ public class IssueService implements ServerComponent {
   public Collection<String> listTags(@Nullable String query, int pageSize) {
     return indexClient.get(IssueIndex.class).listTagsMatching(query, pageSize);
   }
+
+  public Collection<String> setTags(String issueKey, Collection<String> tags) {
+    verifyLoggedIn();
+
+    DbSession session = dbClient.openSession(false);
+    try {
+      DefaultIssue issue = getByKeyForUpdate(session, issueKey).toDefaultIssue();
+      IssueChangeContext context = IssueChangeContext.createUser(new Date(), UserSession.get().login());
+      if (issueUpdater.setTags(issue, tags, context)) {
+        saveIssue(session, issue, context, null);
+      }
+      return issue.tags();
+
+    } finally {
+      session.close();
+    }
+  }
 }
index 5377110c0920b2b4d29ea1c48fd622d6e3534cca..c17a6f782bb7d85213d323147ce780b74ecdb280 100644 (file)
@@ -45,11 +45,13 @@ public class IssuesWs implements WebService {
   private final IssueShowAction showAction;
   private final SearchAction esSearchAction;
   private final TagsAction tagsAction;
+  private final SetTagsAction setTagsAction;
 
-  public IssuesWs(IssueShowAction showAction, SearchAction searchAction, TagsAction tagsAction) {
+  public IssuesWs(IssueShowAction showAction, SearchAction searchAction, TagsAction tagsAction, SetTagsAction setTagsAction) {
     this.showAction = showAction;
     this.esSearchAction = searchAction;
     this.tagsAction = tagsAction;
+    this.setTagsAction = setTagsAction;
   }
 
   @Override
@@ -61,6 +63,7 @@ public class IssuesWs implements WebService {
     showAction.define(controller);
     esSearchAction.define(controller);
     tagsAction.define(controller);
+    setTagsAction.define(controller);
 
     defineChangelogAction(controller);
     defineAssignAction(controller);
diff --git a/server/sonar-server/src/main/java/org/sonar/server/issue/ws/SetTagsAction.java b/server/sonar-server/src/main/java/org/sonar/server/issue/ws/SetTagsAction.java
new file mode 100644 (file)
index 0000000..215e85b
--- /dev/null
@@ -0,0 +1,77 @@
+/*
+ * 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.ws;
+
+import com.google.common.base.Splitter;
+import com.google.common.collect.ImmutableSet;
+import org.sonar.api.server.ws.Request;
+import org.sonar.api.server.ws.RequestHandler;
+import org.sonar.api.server.ws.Response;
+import org.sonar.api.server.ws.WebService;
+import org.sonar.api.server.ws.WebService.NewAction;
+import org.sonar.api.utils.text.JsonWriter;
+import org.sonar.server.issue.IssueService;
+
+import java.util.Collection;
+
+/**
+ * Set tags on an issue.
+ * @since 5.1
+ */
+public class SetTagsAction implements RequestHandler {
+
+  private static final Splitter WS_TAGS_SPLITTER = Splitter.on(',').omitEmptyStrings().trimResults();
+
+  private final IssueService service;
+
+  public SetTagsAction(IssueService service) {
+    this.service = service;
+  }
+
+  void define(WebService.NewController controller) {
+    NewAction action = controller.createAction("set_tags")
+      .setHandler(this)
+      .setPost(true)
+      .setSince("5.1")
+      .setDescription("Set tags on an issue. Requires authentication and Browse permission on project");
+    action.createParam("key")
+      .setDescription("Issue key")
+      .setExampleValue("5bccd6e8-f525-43a2-8d76-fcb13dde79ef")
+      .setRequired(true);
+    action.createParam("tags")
+      .setDescription("A comma separated list of tags")
+      .setExampleValue("security,cwe,misra-c")
+      .setRequired(true)
+      .setDefaultValue("");
+  }
+
+  @Override
+  public void handle(Request request, Response response) throws Exception {
+    String key = request.mandatoryParam("key");
+    String tags = request.mandatoryParam("tags");
+    Collection<String> resultTags = service.setTags(key, ImmutableSet.copyOf(WS_TAGS_SPLITTER.split(tags)));
+    JsonWriter json = response.newJsonWriter().beginObject().name("tags").beginArray();
+    for (String tag : resultTags) {
+      json.value(tag);
+    }
+    json.endArray().endObject().close();
+  }
+
+}
index a1c1308217494c10a9e268818ae6185e907a5e04..4a1b54d23cbfae7eb2e862e6d704d793cbf71dec 100644 (file)
@@ -50,7 +50,13 @@ import org.sonar.core.measure.db.MeasureFilterDao;
 import org.sonar.core.metric.DefaultMetricFinder;
 import org.sonar.core.notification.DefaultNotificationManager;
 import org.sonar.core.permission.PermissionFacade;
-import org.sonar.core.persistence.*;
+import org.sonar.core.persistence.DaoUtils;
+import org.sonar.core.persistence.DatabaseVersion;
+import org.sonar.core.persistence.DefaultDatabase;
+import org.sonar.core.persistence.MyBatis;
+import org.sonar.core.persistence.PreviewDatabaseFactory;
+import org.sonar.core.persistence.SemaphoreUpdater;
+import org.sonar.core.persistence.SemaphoresImpl;
 import org.sonar.core.preview.PreviewCache;
 import org.sonar.core.profiling.Profiling;
 import org.sonar.core.purge.PurgeProfiler;
@@ -76,7 +82,11 @@ import org.sonar.server.activity.index.ActivityNormalizer;
 import org.sonar.server.activity.ws.ActivitiesWebService;
 import org.sonar.server.activity.ws.ActivityMapping;
 import org.sonar.server.authentication.ws.AuthenticationWs;
-import org.sonar.server.batch.*;
+import org.sonar.server.batch.BatchIndex;
+import org.sonar.server.batch.BatchWs;
+import org.sonar.server.batch.GlobalReferentialsAction;
+import org.sonar.server.batch.ProjectReferentialsAction;
+import org.sonar.server.batch.UploadReportAction;
 import org.sonar.server.charts.ChartFactory;
 import org.sonar.server.component.ComponentCleanerService;
 import org.sonar.server.component.ComponentService;
@@ -84,14 +94,34 @@ import org.sonar.server.component.DefaultComponentFinder;
 import org.sonar.server.component.DefaultRubyComponentService;
 import org.sonar.server.component.db.ComponentDao;
 import org.sonar.server.component.db.SnapshotDao;
-import org.sonar.server.component.ws.*;
-import org.sonar.server.computation.*;
+import org.sonar.server.component.ws.ComponentAppAction;
+import org.sonar.server.component.ws.ComponentsWs;
+import org.sonar.server.component.ws.EventsWs;
+import org.sonar.server.component.ws.ProjectsWs;
+import org.sonar.server.component.ws.ResourcesWs;
+import org.sonar.server.computation.AnalysisReportQueue;
+import org.sonar.server.computation.AnalysisReportTaskCleaner;
+import org.sonar.server.computation.AnalysisReportTaskLauncher;
+import org.sonar.server.computation.ComponentIndexationInDatabaseStep;
+import org.sonar.server.computation.ComputationService;
+import org.sonar.server.computation.ComputationStepRegistry;
+import org.sonar.server.computation.DataCleanerStep;
+import org.sonar.server.computation.DigestAnalysisReportStep;
+import org.sonar.server.computation.IndexProjectIssuesStep;
+import org.sonar.server.computation.InvalidatePreviewCacheStep;
+import org.sonar.server.computation.SwitchSnapshotStep;
+import org.sonar.server.computation.SynchronizeProjectPermissionsStep;
 import org.sonar.server.computation.db.AnalysisReportDao;
 import org.sonar.server.computation.dbcleaner.DefaultPurgeTask;
 import org.sonar.server.computation.dbcleaner.IndexPurgeListener;
 import org.sonar.server.computation.dbcleaner.ProjectCleaner;
 import org.sonar.server.computation.dbcleaner.period.DefaultPeriodCleaner;
-import org.sonar.server.computation.ws.*;
+import org.sonar.server.computation.ws.ActiveAnalysisReportsAction;
+import org.sonar.server.computation.ws.AnalysisReportHistorySearchAction;
+import org.sonar.server.computation.ws.AnalysisReportWebService;
+import org.sonar.server.computation.ws.ExperimentalAnalysisReportAction;
+import org.sonar.server.computation.ws.ExperimentalAnalysisReportWebService;
+import org.sonar.server.computation.ws.IsAnalysisReportQueueEmptyAction;
 import org.sonar.server.config.ws.PropertiesWs;
 import org.sonar.server.dashboard.db.DashboardDao;
 import org.sonar.server.dashboard.db.WidgetDao;
@@ -103,7 +133,14 @@ import org.sonar.server.db.DbClient;
 import org.sonar.server.db.EmbeddedDatabaseFactory;
 import org.sonar.server.db.migrations.DatabaseMigrations;
 import org.sonar.server.db.migrations.DatabaseMigrator;
-import org.sonar.server.debt.*;
+import org.sonar.server.debt.DebtCharacteristicsXMLImporter;
+import org.sonar.server.debt.DebtModelBackup;
+import org.sonar.server.debt.DebtModelLookup;
+import org.sonar.server.debt.DebtModelOperations;
+import org.sonar.server.debt.DebtModelPluginRepository;
+import org.sonar.server.debt.DebtModelService;
+import org.sonar.server.debt.DebtModelXMLExporter;
+import org.sonar.server.debt.DebtRulesXMLImporter;
 import org.sonar.server.design.FileDesignWidget;
 import org.sonar.server.duplication.ws.DuplicationsJsonWriter;
 import org.sonar.server.duplication.ws.DuplicationsParser;
@@ -111,17 +148,35 @@ import org.sonar.server.duplication.ws.DuplicationsWs;
 import org.sonar.server.es.EsClient;
 import org.sonar.server.es.IndexCreator;
 import org.sonar.server.es.IndexRegistry;
-import org.sonar.server.issue.*;
+import org.sonar.server.issue.ActionService;
+import org.sonar.server.issue.AssignAction;
+import org.sonar.server.issue.CommentAction;
+import org.sonar.server.issue.InternalRubyIssueService;
+import org.sonar.server.issue.IssueBulkChangeService;
+import org.sonar.server.issue.IssueChangelogFormatter;
+import org.sonar.server.issue.IssueChangelogService;
+import org.sonar.server.issue.IssueCommentService;
+import org.sonar.server.issue.IssueQueryService;
+import org.sonar.server.issue.IssueService;
+import org.sonar.server.issue.PlanAction;
+import org.sonar.server.issue.ServerIssueStorage;
+import org.sonar.server.issue.SetSeverityAction;
+import org.sonar.server.issue.TransitionAction;
 import org.sonar.server.issue.actionplan.ActionPlanService;
 import org.sonar.server.issue.actionplan.ActionPlanWs;
 import org.sonar.server.issue.db.IssueDao;
 import org.sonar.server.issue.filter.IssueFilterService;
 import org.sonar.server.issue.filter.IssueFilterWriter;
 import org.sonar.server.issue.filter.IssueFilterWs;
-import org.sonar.server.issue.index.*;
+import org.sonar.server.issue.index.IssueAuthorizationIndexer;
+import org.sonar.server.issue.index.IssueIndex;
+import org.sonar.server.issue.index.IssueIndexDefinition;
+import org.sonar.server.issue.index.IssueIndexer;
+import org.sonar.server.issue.index.IssueNormalizer;
 import org.sonar.server.issue.ws.IssueActionsWriter;
 import org.sonar.server.issue.ws.IssueShowAction;
 import org.sonar.server.issue.ws.IssuesWs;
+import org.sonar.server.issue.ws.SetTagsAction;
 import org.sonar.server.measure.MeasureFilterEngine;
 import org.sonar.server.measure.MeasureFilterExecutor;
 import org.sonar.server.measure.MeasureFilterFactory;
@@ -140,34 +195,118 @@ import org.sonar.server.platform.ws.L10nWs;
 import org.sonar.server.platform.ws.RestartHandler;
 import org.sonar.server.platform.ws.ServerWs;
 import org.sonar.server.platform.ws.SystemWs;
-import org.sonar.server.plugins.*;
+import org.sonar.server.plugins.InstalledPluginReferentialFactory;
+import org.sonar.server.plugins.PluginDownloader;
+import org.sonar.server.plugins.ServerExtensionInstaller;
+import org.sonar.server.plugins.ServerPluginJarInstaller;
+import org.sonar.server.plugins.ServerPluginJarsInstaller;
+import org.sonar.server.plugins.ServerPluginRepository;
+import org.sonar.server.plugins.UpdateCenterClient;
+import org.sonar.server.plugins.UpdateCenterMatrixFactory;
 import org.sonar.server.properties.ProjectSettingsFactory;
 import org.sonar.server.qualitygate.QgateProjectFinder;
 import org.sonar.server.qualitygate.QualityGates;
 import org.sonar.server.qualitygate.RegisterQualityGates;
-import org.sonar.server.qualitygate.ws.*;
-import org.sonar.server.qualityprofile.*;
+import org.sonar.server.qualitygate.ws.QGatesAppAction;
+import org.sonar.server.qualitygate.ws.QGatesCopyAction;
+import org.sonar.server.qualitygate.ws.QGatesCreateAction;
+import org.sonar.server.qualitygate.ws.QGatesCreateConditionAction;
+import org.sonar.server.qualitygate.ws.QGatesDeleteConditionAction;
+import org.sonar.server.qualitygate.ws.QGatesDeselectAction;
+import org.sonar.server.qualitygate.ws.QGatesDestroyAction;
+import org.sonar.server.qualitygate.ws.QGatesListAction;
+import org.sonar.server.qualitygate.ws.QGatesRenameAction;
+import org.sonar.server.qualitygate.ws.QGatesSearchAction;
+import org.sonar.server.qualitygate.ws.QGatesSelectAction;
+import org.sonar.server.qualitygate.ws.QGatesSetAsDefaultAction;
+import org.sonar.server.qualitygate.ws.QGatesShowAction;
+import org.sonar.server.qualitygate.ws.QGatesUnsetDefaultAction;
+import org.sonar.server.qualitygate.ws.QGatesUpdateConditionAction;
+import org.sonar.server.qualitygate.ws.QGatesWs;
+import org.sonar.server.qualityprofile.BuiltInProfiles;
+import org.sonar.server.qualityprofile.QProfileBackuper;
+import org.sonar.server.qualityprofile.QProfileCopier;
+import org.sonar.server.qualityprofile.QProfileExporters;
+import org.sonar.server.qualityprofile.QProfileFactory;
+import org.sonar.server.qualityprofile.QProfileLoader;
+import org.sonar.server.qualityprofile.QProfileLookup;
+import org.sonar.server.qualityprofile.QProfileProjectLookup;
+import org.sonar.server.qualityprofile.QProfileProjectOperations;
+import org.sonar.server.qualityprofile.QProfileReset;
+import org.sonar.server.qualityprofile.QProfileService;
+import org.sonar.server.qualityprofile.QProfiles;
+import org.sonar.server.qualityprofile.RegisterQualityProfiles;
+import org.sonar.server.qualityprofile.RuleActivator;
+import org.sonar.server.qualityprofile.RuleActivatorContextFactory;
 import org.sonar.server.qualityprofile.db.ActiveRuleDao;
 import org.sonar.server.qualityprofile.index.ActiveRuleIndex;
 import org.sonar.server.qualityprofile.index.ActiveRuleNormalizer;
-import org.sonar.server.qualityprofile.ws.*;
-import org.sonar.server.rule.*;
+import org.sonar.server.qualityprofile.ws.BulkRuleActivationActions;
+import org.sonar.server.qualityprofile.ws.ProfilesWs;
+import org.sonar.server.qualityprofile.ws.QProfileRestoreBuiltInAction;
+import org.sonar.server.qualityprofile.ws.QProfilesWs;
+import org.sonar.server.qualityprofile.ws.RuleActivationActions;
+import org.sonar.server.rule.DefaultRuleFinder;
+import org.sonar.server.rule.DeprecatedRulesDefinitionLoader;
+import org.sonar.server.rule.RegisterRules;
+import org.sonar.server.rule.RubyRuleService;
+import org.sonar.server.rule.RuleCreator;
+import org.sonar.server.rule.RuleDefinitionsLoader;
+import org.sonar.server.rule.RuleDeleter;
+import org.sonar.server.rule.RuleOperations;
+import org.sonar.server.rule.RuleRepositories;
+import org.sonar.server.rule.RuleService;
+import org.sonar.server.rule.RuleUpdater;
 import org.sonar.server.rule.db.RuleDao;
 import org.sonar.server.rule.index.RuleIndex;
 import org.sonar.server.rule.index.RuleNormalizer;
-import org.sonar.server.rule.ws.*;
-import org.sonar.server.search.*;
+import org.sonar.server.rule.ws.ActiveRuleCompleter;
+import org.sonar.server.rule.ws.AppAction;
+import org.sonar.server.rule.ws.DeleteAction;
+import org.sonar.server.rule.ws.RuleMapping;
+import org.sonar.server.rule.ws.RulesWebService;
+import org.sonar.server.rule.ws.SearchAction;
+import org.sonar.server.rule.ws.TagsAction;
+import org.sonar.server.rule.ws.UpdateAction;
+import org.sonar.server.search.IndexClient;
+import org.sonar.server.search.IndexQueue;
+import org.sonar.server.search.IndexSynchronizer;
+import org.sonar.server.search.SearchClient;
+import org.sonar.server.search.SearchHealth;
 import org.sonar.server.source.HtmlSourceDecorator;
 import org.sonar.server.source.IndexSourceLinesStep;
 import org.sonar.server.source.SourceService;
 import org.sonar.server.source.index.SourceLineIndex;
 import org.sonar.server.source.index.SourceLineIndexDefinition;
 import org.sonar.server.source.index.SourceLineIndexer;
-import org.sonar.server.source.ws.*;
+import org.sonar.server.source.ws.HashAction;
+import org.sonar.server.source.ws.IndexAction;
+import org.sonar.server.source.ws.LinesAction;
+import org.sonar.server.source.ws.RawAction;
+import org.sonar.server.source.ws.ScmAction;
+import org.sonar.server.source.ws.ScmWriter;
 import org.sonar.server.source.ws.ShowAction;
-import org.sonar.server.startup.*;
+import org.sonar.server.source.ws.SourcesWs;
+import org.sonar.server.startup.CleanPreviewAnalysisCache;
+import org.sonar.server.startup.CopyRequirementsFromCharacteristicsToRules;
+import org.sonar.server.startup.GeneratePluginIndex;
+import org.sonar.server.startup.JdbcDriverDeployer;
+import org.sonar.server.startup.LogServerId;
+import org.sonar.server.startup.RegisterDashboards;
+import org.sonar.server.startup.RegisterDebtModel;
+import org.sonar.server.startup.RegisterMetrics;
+import org.sonar.server.startup.RegisterNewMeasureFilters;
+import org.sonar.server.startup.RegisterPermissionTemplates;
+import org.sonar.server.startup.RegisterServletFilters;
+import org.sonar.server.startup.RenameDeprecatedPropertyKeys;
+import org.sonar.server.startup.ServerMetadataPersister;
 import org.sonar.server.test.CoverageService;
-import org.sonar.server.test.ws.*;
+import org.sonar.server.test.ws.CoverageShowAction;
+import org.sonar.server.test.ws.CoverageWs;
+import org.sonar.server.test.ws.TestsCoveredFilesAction;
+import org.sonar.server.test.ws.TestsShowAction;
+import org.sonar.server.test.ws.TestsTestCasesAction;
+import org.sonar.server.test.ws.TestsWs;
 import org.sonar.server.text.MacroInterpreter;
 import org.sonar.server.text.RubyTextService;
 import org.sonar.server.ui.JRubyI18n;
@@ -175,12 +314,23 @@ import org.sonar.server.ui.JRubyProfiling;
 import org.sonar.server.ui.PageDecorations;
 import org.sonar.server.ui.Views;
 import org.sonar.server.updatecenter.ws.UpdateCenterWs;
-import org.sonar.server.user.*;
+import org.sonar.server.user.DefaultUserService;
+import org.sonar.server.user.DoPrivileged;
+import org.sonar.server.user.GroupMembershipFinder;
+import org.sonar.server.user.GroupMembershipService;
+import org.sonar.server.user.NewUserNotifier;
+import org.sonar.server.user.SecurityRealmFactory;
 import org.sonar.server.user.db.GroupDao;
 import org.sonar.server.user.ws.FavoritesWs;
 import org.sonar.server.user.ws.UserPropertiesWs;
 import org.sonar.server.user.ws.UsersWs;
-import org.sonar.server.util.*;
+import org.sonar.server.util.BooleanTypeValidation;
+import org.sonar.server.util.FloatTypeValidation;
+import org.sonar.server.util.IntegerTypeValidation;
+import org.sonar.server.util.StringListTypeValidation;
+import org.sonar.server.util.StringTypeValidation;
+import org.sonar.server.util.TextTypeValidation;
+import org.sonar.server.util.TypeValidations;
 import org.sonar.server.ws.ListingWs;
 import org.sonar.server.ws.WebServiceEngine;
 
@@ -503,6 +653,7 @@ class ServerComponents {
     pico.addSingleton(IssueShowAction.class);
     pico.addSingleton(org.sonar.server.issue.ws.SearchAction.class);
     pico.addSingleton(org.sonar.server.issue.ws.TagsAction.class);
+    pico.addSingleton(SetTagsAction.class);
     pico.addSingleton(IssueService.class);
     pico.addSingleton(IssueActionsWriter.class);
     pico.addSingleton(IssueQueryService.class);
index 47668c6c9638184b80062c08e77a282e18a3c050..a94bdba08466beed08288d94406fb5d5ef0fe157 100644 (file)
@@ -19,7 +19,9 @@
  */
 package org.sonar.server.issue;
 
+import com.google.common.collect.ImmutableSet;
 import com.google.common.collect.Multiset;
+import com.google.common.collect.Sets;
 import org.junit.After;
 import org.junit.Before;
 import org.junit.ClassRule;
@@ -433,4 +435,55 @@ public class IssueServiceMediumTest {
   private IssueDto newIssue() {
     return IssueTesting.newDto(rule, file, project);
   }
+
+  @Test
+  public void list_tags() {
+    db.issueDao().insert(session,
+      IssueTesting.newDto(rule, file, project).setTags(ImmutableSet.of("convention", "java8", "bug")),
+      IssueTesting.newDto(rule, file, project).setTags(ImmutableSet.of("convention", "bug")),
+      IssueTesting.newDto(rule, file, project),
+      IssueTesting.newDto(rule, file, project).setTags(ImmutableSet.of("convention")));
+    session.commit();
+    index();
+
+    assertThat(service.listTags(null, 5)).containsOnly("convention", "java8", "bug");
+    assertThat(service.listTags(null, 2)).containsOnly("bug", "convention");
+    assertThat(service.listTags("vent", 5)).containsOnly("convention");
+    assertThat(service.listTags(null, 1)).containsOnly("bug");
+    assertThat(service.listTags(null, Integer.MAX_VALUE)).containsOnly("convention", "java8", "bug");
+  }
+
+  @Test
+  public void set_tags() {
+    IssueDto issue = newIssue();
+    tester.get(IssueDao.class).insert(session, issue);
+
+    session.commit();
+    index();
+
+    assertThat(indexClient.get(IssueIndex.class).getByKey(issue.getKey()).tags()).isEmpty();
+
+    // Tags are lowercased
+    service.setTags(issue.getKey(), ImmutableSet.of("bug", "Convention"));
+    assertThat(indexClient.get(IssueIndex.class).getByKey(issue.getKey()).tags()).containsOnly("bug", "convention");
+
+    // nulls and empty tags are ignored
+    service.setTags(issue.getKey(), Sets.newHashSet("security", null, "", "convention"));
+    assertThat(indexClient.get(IssueIndex.class).getByKey(issue.getKey()).tags()).containsOnly("security", "convention");
+
+    // tag validation
+    try {
+      service.setTags(issue.getKey(), ImmutableSet.of("pol op"));
+    } catch (Exception exception) {
+      assertThat(exception).isInstanceOf(IllegalArgumentException.class);
+    }
+    assertThat(indexClient.get(IssueIndex.class).getByKey(issue.getKey()).tags()).containsOnly("security", "convention");
+
+    // unchanged tags
+    service.setTags(issue.getKey(), ImmutableSet.of("convention", "security"));
+    assertThat(indexClient.get(IssueIndex.class).getByKey(issue.getKey()).tags()).containsOnly("security", "convention");
+
+    service.setTags(issue.getKey(), ImmutableSet.<String>of());
+    assertThat(indexClient.get(IssueIndex.class).getByKey(issue.getKey()).tags()).isEmpty();
+  }
 }
index 5a37163006ce4dd287a657b8cb4ceb0766d935ba..b791878ab8d73efad392bb7522a049a0ecffa23c 100644 (file)
@@ -49,7 +49,12 @@ import org.sonar.server.component.ComponentTesting;
 import org.sonar.server.component.db.ComponentDao;
 import org.sonar.server.db.DbClient;
 import org.sonar.server.debt.DebtModelService;
-import org.sonar.server.issue.*;
+import org.sonar.server.issue.ActionService;
+import org.sonar.server.issue.IssueChangelog;
+import org.sonar.server.issue.IssueChangelogService;
+import org.sonar.server.issue.IssueCommentService;
+import org.sonar.server.issue.IssueQueryService;
+import org.sonar.server.issue.IssueService;
 import org.sonar.server.issue.actionplan.ActionPlanService;
 import org.sonar.server.rule.Rule;
 import org.sonar.server.rule.RuleService;
@@ -145,7 +150,7 @@ public class IssueShowActionTest {
       new SearchAction(mock(DbClient.class), mock(IssueChangeDao.class), mock(IssueService.class), mock(IssueActionsWriter.class), mock(IssueQueryService.class),
         mock(RuleService.class),
         mock(ActionPlanService.class), mock(UserFinder.class), mock(I18n.class), mock(Durations.class), mock(Languages.class)),
-      new TagsAction(null)
+      new TagsAction(null), new SetTagsAction(null)
       ));
   }
 
index 9116cf8f22a5efccbfe1bb7c45b21bd6cb8544c0..897e71e24de2804914671d2bb95ea28633cd04af 100644 (file)
 package org.sonar.server.issue.ws;
 
 import com.google.common.collect.Lists;
-
 import org.junit.Before;
-import org.sonar.server.issue.IssueService;
+import org.junit.Test;
+import org.junit.runner.RunWith;
 import org.mockito.Mock;
 import org.mockito.runners.MockitoJUnitRunner;
-import org.junit.runner.RunWith;
-import org.sonar.api.server.ws.WebService.Param;
-import org.junit.Test;
 import org.sonar.api.server.ws.WebService.Action;
+import org.sonar.api.server.ws.WebService.Param;
+import org.sonar.server.issue.IssueService;
 import org.sonar.server.ws.WsTester;
+
 import static org.fest.assertions.Assertions.assertThat;
 import static org.mockito.Mockito.verify;
 import static org.mockito.Mockito.when;
@@ -50,7 +50,7 @@ public class IssueTagsActionTest {
     tester = new WsTester(
       new IssuesWs(new IssueShowAction(null, null, null, null, null, null, null, null, null, null, null),
         new SearchAction(null, null, null, null, null, null, null, null, null, null,null),
-        tagsAction));
+        tagsAction, new SetTagsAction(null)));
   }
 
   @Test
index 6cb48387b896b2e51337a0f7629fddd84983c974..8c3f3d0e4bab5621002649a680899e50f93c93a4 100644 (file)
@@ -37,6 +37,7 @@ import org.sonar.server.issue.IssueService;
 import org.sonar.server.issue.actionplan.ActionPlanService;
 import org.sonar.server.rule.RuleService;
 import org.sonar.server.ws.WsTester;
+
 import static org.fest.assertions.Assertions.assertThat;
 import static org.mockito.Mockito.mock;
 
@@ -60,7 +61,7 @@ public class IssuesWsTest {
     SearchAction searchAction = new SearchAction(mock(DbClient.class), mock(IssueChangeDao.class), mock(IssueService.class), mock(IssueActionsWriter.class),
       mock(IssueQueryService.class), mock(RuleService.class),
       mock(ActionPlanService.class), mock(UserFinder.class), mock(I18n.class), mock(Durations.class), mock(Languages.class));
-    tester = new WsTester(new IssuesWs(showAction, searchAction, new TagsAction(null)));
+    tester = new WsTester(new IssuesWs(showAction, searchAction, new TagsAction(null), new SetTagsAction(null)));
   }
 
   @Test
@@ -69,7 +70,7 @@ public class IssuesWsTest {
     assertThat(controller).isNotNull();
     assertThat(controller.description()).isNotEmpty();
     assertThat(controller.since()).isEqualTo("3.6");
-    assertThat(controller.actions()).hasSize(15);
+    assertThat(controller.actions()).hasSize(16);
   }
 
   @Test
diff --git a/server/sonar-server/src/test/java/org/sonar/server/issue/ws/SetTagsActionTest.java b/server/sonar-server/src/test/java/org/sonar/server/issue/ws/SetTagsActionTest.java
new file mode 100644 (file)
index 0000000..37ceace
--- /dev/null
@@ -0,0 +1,84 @@
+/*
+ * 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.ws;
+
+import com.google.common.collect.ImmutableSet;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mock;
+import org.mockito.runners.MockitoJUnitRunner;
+import org.sonar.api.server.ws.WebService.Action;
+import org.sonar.api.server.ws.WebService.Param;
+import org.sonar.server.issue.IssueService;
+import org.sonar.server.ws.WsTester;
+
+import static org.fest.assertions.Assertions.assertThat;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+@RunWith(MockitoJUnitRunner.class)
+public class SetTagsActionTest {
+
+  @Mock
+  private IssueService service;
+
+  private SetTagsAction setTagsAction;
+
+  private WsTester tester;
+
+  @Before
+  public void setUp() {
+    setTagsAction = new SetTagsAction(service);
+    tester = new WsTester(
+      new IssuesWs(new IssueShowAction(null, null, null, null, null, null, null, null, null, null, null),
+        new SearchAction(null, null, null, null, null, null, null, null, null, null,null),
+        new TagsAction(null), setTagsAction));
+  }
+
+  @Test
+  public void should_define() throws Exception {
+    Action action = tester.controller("api/issues").action("set_tags");
+    assertThat(action.description()).isNotEmpty();
+    assertThat(action.responseExampleAsString()).isNull();
+    assertThat(action.isPost()).isTrue();
+    assertThat(action.isInternal()).isFalse();
+    assertThat(action.handler()).isEqualTo(setTagsAction);
+    assertThat(action.params()).hasSize(2);
+
+    Param query = action.param("key");
+    assertThat(query.isRequired()).isTrue();
+    assertThat(query.description()).isNotEmpty();
+    assertThat(query.exampleValue()).isNotEmpty();
+    Param pageSize = action.param("tags");
+    assertThat(pageSize.isRequired()).isTrue();
+    assertThat(pageSize.defaultValue()).isEqualTo("");
+    assertThat(pageSize.description()).isNotEmpty();
+    assertThat(pageSize.exampleValue()).isNotEmpty();
+  }
+
+  @Test
+  public void should_set_tags() throws Exception {
+    when(service.setTags("polop", ImmutableSet.of("palap"))).thenReturn(ImmutableSet.of("palap"));
+    tester.newPostRequest("api/issues", "set_tags").setParam("key", "polop").setParam("tags", "palap").execute()
+      .assertJson("{\"tags\":[\"palap\"]}");
+    verify(service).setTags("polop", ImmutableSet.of("palap"));
+  }
+}
index 85628de373e449e5cb1e1ffb9fff25bac1facbfa..28ce4e2c26c4397c8af01e3c5567c9679c178f08 100644 (file)
  */
 package org.sonar.core.issue;
 
+import com.google.common.base.Function;
+import com.google.common.base.Joiner;
 import com.google.common.base.Objects;
+import com.google.common.base.Predicate;
+import com.google.common.collect.Collections2;
+import org.apache.commons.collections.CollectionUtils;
 import org.apache.commons.lang.StringUtils;
 import org.apache.commons.lang.time.DateUtils;
 import org.sonar.api.BatchComponent;
@@ -28,12 +33,14 @@ import org.sonar.api.issue.ActionPlan;
 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.server.rule.RuleTagFormat;
 import org.sonar.api.user.User;
 import org.sonar.api.utils.Duration;
 
 import javax.annotation.Nullable;
 
 import java.util.Calendar;
+import java.util.Collection;
 import java.util.Date;
 
 /**
@@ -49,6 +56,9 @@ public class IssueUpdater implements BatchComponent, ServerComponent {
   public static final String AUTHOR = "author";
   public static final String ACTION_PLAN = "actionPlan";
   public static final String TECHNICAL_DEBT = "technicalDebt";
+  public static final String TAGS = "tags";
+
+  private static final Joiner CHANGELOG_TAG_JOINER = Joiner.on(" ").skipNulls();
 
   public boolean setSeverity(DefaultIssue issue, String severity, IssueChangeContext context) {
     if (issue.manualSeverity()) {
@@ -263,4 +273,34 @@ public class IssueUpdater implements BatchComponent, ServerComponent {
     return setProject(issue, currentProjectKey, context);
   }
 
+  public boolean setTags(DefaultIssue issue, Collection<String> tags, IssueChangeContext context) {
+    Collection<String> newTags = Collections2.transform(
+      Collections2.filter(tags, new Predicate<String>() {
+        @Override
+        public boolean apply(String tag) {
+          return tag != null && !tag.isEmpty();
+        }
+      }), new Function<String, String>() {
+        @Override
+        public String apply(String tag) {
+          String lowerCaseTag = tag.toLowerCase();
+          RuleTagFormat.validate(lowerCaseTag);
+          return lowerCaseTag;
+        }
+      });
+
+    Collection<String> oldTags = issue.tags();
+    if (!CollectionUtils.isEqualCollection(oldTags, newTags)) {
+      issue.setFieldChange(context, TAGS,
+        oldTags.isEmpty() ? null : CHANGELOG_TAG_JOINER.join(oldTags),
+        newTags.isEmpty() ? null : CHANGELOG_TAG_JOINER.join(newTags));
+      issue.setTags(newTags);
+      issue.setUpdateDate(context.date());
+      issue.setChanged(true);
+      issue.setSendNotifications(true);
+      return true;
+    }
+    return false;
+  }
+
 }
index b201abbf812124ae068e42b359af5eba9b9c31c4..53a8cd5064a6e643c63b3b582a284ffdf63bd124 100644 (file)
  */
 package org.sonar.core.issue.db;
 
+import com.google.common.base.Joiner;
 import com.google.common.base.Objects;
 import com.google.common.base.Preconditions;
+import com.google.common.base.Splitter;
+import com.google.common.collect.ImmutableSet;
 import org.apache.commons.lang.builder.ToStringBuilder;
 import org.apache.commons.lang.builder.ToStringStyle;
 import org.sonar.api.issue.internal.DefaultIssue;
@@ -36,6 +39,7 @@ import javax.annotation.CheckForNull;
 import javax.annotation.Nullable;
 
 import java.io.Serializable;
+import java.util.Collection;
 import java.util.Date;
 
 /**
@@ -43,6 +47,10 @@ import java.util.Date;
  */
 public final class IssueDto implements Serializable {
 
+  private static final char TAGS_SEPARATOR = ',';
+  private static final Joiner TAGS_JOINER = Joiner.on(TAGS_SEPARATOR).skipNulls();
+  private static final Splitter TAGS_SPLITTER = Splitter.on(',').trimResults().omitEmptyStrings();
+
   private Long id;
   private String kee;
   private Long componentId;
@@ -86,6 +94,7 @@ public final class IssueDto implements Serializable {
   private String projectKey;
   private String projectUuid;
   private String filePath;
+  private String tags;
 
   public String getKey() {
     return getKee();
@@ -522,6 +531,28 @@ public final class IssueDto implements Serializable {
     return this;
   }
 
+  public Collection<String> getTags() {
+    return ImmutableSet.copyOf(TAGS_SPLITTER.split(tags == null ? "" : tags));
+  }
+
+  public IssueDto setTags(Collection<String> tags) {
+    if (tags.isEmpty()) {
+      this.tags = null;
+    } else {
+      this.tags = TAGS_JOINER.join(tags);
+    }
+    return this;
+  }
+
+  public String getTagsString() {
+    return tags;
+  }
+
+  public IssueDto setTagsString(String tags) {
+    this.tags = tags;
+    return this;
+  }
+
   @Override
   public String toString() {
     return ToStringBuilder.reflectionToString(this, ToStringStyle.SHORT_PREFIX_STYLE);
@@ -546,6 +577,7 @@ public final class IssueDto implements Serializable {
       .setAssignee(issue.assignee())
       .setRuleId(ruleId)
       .setRuleKey(issue.ruleKey().repository(), issue.ruleKey().rule())
+      .setTags(issue.tags())
       .setComponentUuid(issue.componentUuid())
       .setComponentId(componentId)
       .setComponentKey(issue.componentKey())
@@ -596,6 +628,7 @@ public final class IssueDto implements Serializable {
       .setIssueAttributes(KeyValueFormat.format(issue.attributes()))
       .setAuthorLogin(issue.authorLogin())
       .setRuleKey(issue.ruleKey().repository(), issue.ruleKey().rule())
+      .setTags(issue.tags())
       .setComponentUuid(issue.componentUuid())
       .setComponentKey(issue.componentKey())
       .setModuleUuid(issue.moduleUuid())
@@ -634,6 +667,7 @@ public final class IssueDto implements Serializable {
     issue.setProjectKey(projectKey);
     issue.setManualSeverity(manualSeverity);
     issue.setRuleKey(getRuleKey());
+    issue.setTags(getTags());
     issue.setLanguage(language);
     issue.setActionPlanKey(actionPlanKey);
     issue.setAuthorLogin(authorLogin);
index 26f0850046647cef061143068aeb6883d126bd79..ed35595120f0d58666c524c246e57cc59144a7a4 100644 (file)
@@ -23,6 +23,7 @@
     i.reporter as reporter,
     i.assignee as assignee,
     i.author_login as authorLogin,
+    i.tags as tagsString,
     i.issue_attributes as issueAttributes,
     i.issue_creation_date as issueCreationDate,
     i.issue_update_date as issueUpdateDate,
 
   <insert id="insert" parameterType="Issue" useGeneratedKeys="false" keyProperty="id">
     INSERT INTO issues (kee, component_id, root_component_id, rule_id, action_plan_key, severity, manual_severity,
-    message, line, effort_to_fix, technical_debt, status,
+    message, line, effort_to_fix, technical_debt, status, tags,
     resolution, checksum, reporter, assignee, author_login, issue_attributes, issue_creation_date, issue_update_date,
     issue_close_date, created_at, updated_at)
     VALUES (#{kee}, #{componentId}, #{projectId}, #{ruleId}, #{actionPlanKey}, #{severity}, #{manualSeverity},
-    #{message}, #{line}, #{effortToFix}, #{debt}, #{status},
+    #{message}, #{line}, #{effortToFix}, #{debt}, #{status}, #{tagsString},
     #{resolution}, #{checksum}, #{reporter}, #{assignee}, #{authorLogin}, #{issueAttributes}, #{issueCreationDate},
     #{issueUpdateDate}, #{issueCloseDate}, #{createdAt}, #{updatedAt})
   </insert>
@@ -95,6 +96,7 @@
     reporter=#{reporter},
     assignee=#{assignee},
     author_login=#{authorLogin},
+    tags=#{tagsString},
     root_component_id=#{projectId},
     issue_attributes=#{issueAttributes},
     issue_creation_date=#{issueCreationDate},
     reporter=#{reporter},
     assignee=#{assignee},
     author_login=#{authorLogin},
+    tags=#{tagsString},
     root_component_id=#{projectId},
     issue_attributes=#{issueAttributes},
     issue_creation_date=#{issueCreationDate},
     i.reporter as reporter,
     i.assignee as assignee,
     i.author_login as authorLogin,
+    i.tags as tagsString,
     i.issue_attributes as issueAttributes,
     i.issue_creation_date as issueCreationDate,
     i.issue_update_date as issueUpdateDate,
index 0a84ee812b68456f4c3e6e951057e259ce54de9d..29fafb50953424e90e61791b1dc248962f280821 100644 (file)
@@ -705,6 +705,7 @@ issue.changelog.field.author=Author
 issue.changelog.field.resolution=Resolution
 issue.changelog.field.technicalDebt=Technical Debt
 issue.changelog.field.status=Status
+issue.changelog.field.tags=Tags
 
 
 #------------------------------------------------------------------------------
index 55caecffcae5e68cdc0b70583e31daeaa03bc67f..391830e4131644d4ab0ae3a0299315321f73f1d5 100644 (file)
@@ -26,6 +26,7 @@ import org.sonar.api.utils.Duration;
 import javax.annotation.CheckForNull;
 
 import java.io.Serializable;
+import java.util.Collection;
 import java.util.Date;
 import java.util.List;
 import java.util.Map;
@@ -201,4 +202,9 @@ public interface Issue extends Serializable {
    * @since 5.0
    */
   String componentUuid();
+
+  /**
+   * @since 5.1
+   */
+  Collection<String> tags();
 }
index e0f151ac9d0b24813dfb5a8783482e8d7ab8bb62..2542aeb19d52a26511e812c9550b698b88f64a60 100644 (file)
@@ -24,6 +24,7 @@ import com.google.common.base.Preconditions;
 import com.google.common.base.Strings;
 import com.google.common.collect.ImmutableList;
 import com.google.common.collect.ImmutableMap;
+import com.google.common.collect.ImmutableSet;
 import com.google.common.collect.Maps;
 import org.apache.commons.lang.StringUtils;
 import org.apache.commons.lang.builder.ToStringBuilder;
@@ -39,7 +40,13 @@ import javax.annotation.CheckForNull;
 import javax.annotation.Nullable;
 
 import java.io.Serializable;
-import java.util.*;
+import java.util.Calendar;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.Date;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
 
 import static com.google.common.collect.Lists.newArrayList;
 
@@ -79,6 +86,7 @@ public class DefaultIssue implements Issue {
   private String authorLogin = null;
   private String actionPlanKey;
   private List<IssueComment> comments = null;
+  private Set<String> tags = null;
 
   // FUNCTIONAL DATES
   private Date creationDate;
@@ -582,4 +590,17 @@ public class DefaultIssue implements Issue {
     return ToStringBuilder.reflectionToString(this, ToStringStyle.SHORT_PREFIX_STYLE);
   }
 
+  @Override
+  public Collection<String> tags() {
+    if (tags == null) {
+      return ImmutableSet.of();
+    } else {
+      return ImmutableSet.copyOf(tags);
+    }
+  }
+
+  public DefaultIssue setTags(Collection<String> tags) {
+    this.tags = ImmutableSet.copyOf(tags);
+    return this;
+  }
 }