]> source.dussan.org Git - sonarqube.git/commitdiff
SONAR-10199 Fix search of issues by author containing comma
authorJulien Lancelot <julien.lancelot@sonarsource.com>
Tue, 22 Jan 2019 12:00:53 +0000 (13:00 +0100)
committersonartech <sonartech@sonarsource.com>
Mon, 11 Feb 2019 08:11:42 +0000 (09:11 +0100)
12 files changed:
plugins/sonar-xoo-plugin/build.gradle
plugins/sonar-xoo-plugin/src/main/java/org/sonar/xoo/scm/XooBlameCommand.java
plugins/sonar-xoo-plugin/src/test/java/org/sonar/xoo/scm/XooBlameCommandTest.java
server/sonar-server/src/main/java/org/sonar/server/issue/index/IssueIndex.java
server/sonar-server/src/main/java/org/sonar/server/issue/ws/SearchAction.java
server/sonar-server/src/test/java/org/sonar/server/issue/index/IssueIndexFacetsTest.java
server/sonar-server/src/test/java/org/sonar/server/issue/ws/SearchActionComponentsTest.java
server/sonar-server/src/test/java/org/sonar/server/issue/ws/SearchActionTest.java
server/sonar-server/src/test/resources/org/sonar/server/issue/ws/SearchActionComponentsTest/search_by_authors.json [deleted file]
sonar-ws/src/main/java/org/sonarqube/ws/client/issue/IssuesWsParameters.java
sonar-ws/src/main/java/org/sonarqube/ws/client/issues/IssuesService.java
sonar-ws/src/main/java/org/sonarqube/ws/client/issues/SearchRequest.java

index 66c6d051b68d21473705f2d47179c4c3a15f64e2..693f0cb1363c571e185d46070f805bc12972a304 100644 (file)
@@ -6,6 +6,7 @@ dependencies {
   compile 'com.google.guava:guava'
   compile 'commons-io:commons-io'
   compile 'commons-lang:commons-lang'
+  compile 'org.apache.commons:commons-csv'
   compileOnly 'com.google.code.findbugs:jsr305'
   runtime project(path: ':sonar-plugin-api', configuration: 'shadow')
   compileOnly project(path: ':sonar-plugin-api')
index 6138d7d5ba57d923789a0136104449fe848f4956..9a803dd3b4ce5531a458d4fe84fc3fa1df4171f7 100644 (file)
@@ -21,18 +21,22 @@ package org.sonar.xoo.scm;
 
 import com.google.common.annotations.VisibleForTesting;
 import java.io.File;
+import java.io.FileReader;
 import java.io.IOException;
-import java.nio.charset.StandardCharsets;
-import java.util.ArrayList;
 import java.util.Date;
 import java.util.List;
-import org.apache.commons.io.FileUtils;
-import org.apache.commons.lang.StringUtils;
+import org.apache.commons.csv.CSVFormat;
+import org.apache.commons.csv.CSVParser;
+import org.apache.commons.csv.CSVRecord;
 import org.sonar.api.batch.fs.InputFile;
 import org.sonar.api.batch.scm.BlameCommand;
 import org.sonar.api.batch.scm.BlameLine;
 import org.sonar.api.utils.DateUtils;
 
+import static com.google.common.base.Preconditions.checkState;
+import static java.util.stream.Collectors.toList;
+import static org.apache.commons.lang.StringUtils.trimToNull;
+
 public class XooBlameCommand extends BlameCommand {
 
   private static final String SCM_EXTENSION = ".scm";
@@ -47,42 +51,46 @@ public class XooBlameCommand extends BlameCommand {
   @VisibleForTesting
   protected void processFile(InputFile inputFile, BlameOutput result) {
     File ioFile = inputFile.file();
-    File scmDataFile = new java.io.File(ioFile.getParentFile(), ioFile.getName() + SCM_EXTENSION);
+    File scmDataFile = new File(ioFile.getParentFile(), ioFile.getName() + SCM_EXTENSION);
     if (!scmDataFile.exists()) {
       return;
     }
 
     try {
-      List<String> lines = FileUtils.readLines(scmDataFile, StandardCharsets.UTF_8);
-      List<BlameLine> blame = new ArrayList<>(lines.size());
-      int lineNumber = 0;
-      for (String line : lines) {
-        lineNumber++;
-        if (StringUtils.isNotBlank(line)) {
-          // revision,author,dateTime
-          String[] fields = StringUtils.splitPreserveAllTokens(line, ',');
-          if (fields.length < 3) {
-            throw new IllegalStateException("Not enough fields on line " + lineNumber);
-          }
-          String revision = StringUtils.trimToNull(fields[0]);
-          String author = StringUtils.trimToNull(fields[1]);
-          BlameLine blameLine = new BlameLine().revision(revision).author(author);
-          String dateStr = StringUtils.trimToNull(fields[2]);
-          if (dateStr != null) {
-            Date dateTime = DateUtils.parseDateTimeQuietly(dateStr);
-            if (dateTime != null) {
-              blameLine.date(dateTime);
-            } else {
-              // Will throw an exception, when date is not in format "yyyy-MM-dd"
-              blameLine.date(DateUtils.parseDate(dateStr));
-            }
-          }
-          blame.add(blameLine);
-        }
-      }
+      List<BlameLine> blame = readFile(scmDataFile);
       result.blameResult(inputFile, blame);
     } catch (IOException e) {
       throw new IllegalStateException(e);
     }
   }
+
+  private static List<BlameLine> readFile(File inputStream) throws IOException {
+    try (CSVParser csvParser = CSVFormat.RFC4180
+      .withIgnoreEmptyLines()
+      .withIgnoreSurroundingSpaces()
+      .parse(new FileReader(inputStream))) {
+      List<CSVRecord> records = csvParser.getRecords();
+      return records.stream()
+        .map(XooBlameCommand::convertToBlameLine)
+        .collect(toList());
+    }
+  }
+
+  private static BlameLine convertToBlameLine(CSVRecord csvRecord) {
+    checkState(csvRecord.size() == 3, "Not enough fields on line %s", csvRecord);
+    String revision = trimToNull(csvRecord.get(0));
+    String author = trimToNull(csvRecord.get(1));
+    BlameLine blameLine = new BlameLine().revision(revision).author(author);
+    String dateStr = trimToNull(csvRecord.get(2));
+    if (dateStr != null) {
+      Date dateTime = DateUtils.parseDateTimeQuietly(dateStr);
+      if (dateTime != null) {
+        blameLine.date(dateTime);
+      } else {
+        // Will throw an exception, when date is not in format "yyyy-MM-dd"
+        blameLine.date(DateUtils.parseDate(dateStr));
+      }
+    }
+    return blameLine;
+  }
 }
index bb95b62e7f1661f56a095589b8c43b486db7cc58..9f9a1a22b0e778a7d0b317fbeb4d6b1654788216 100644 (file)
  */
 package org.sonar.xoo.scm;
 
+import java.io.File;
+import java.io.IOException;
+import java.util.Arrays;
 import org.apache.commons.io.FileUtils;
 import org.junit.Before;
 import org.junit.Rule;
 import org.junit.Test;
 import org.junit.rules.ExpectedException;
 import org.junit.rules.TemporaryFolder;
-import org.sonar.api.batch.fs.InputFile;
 import org.sonar.api.batch.fs.internal.DefaultFileSystem;
 import org.sonar.api.batch.fs.internal.DefaultInputFile;
 import org.sonar.api.batch.fs.internal.TestInputFileBuilder;
@@ -35,10 +37,7 @@ import org.sonar.api.batch.scm.BlameLine;
 import org.sonar.api.utils.DateUtils;
 import org.sonar.xoo.Xoo;
 
-import java.io.File;
-import java.io.IOException;
-import java.util.Arrays;
-
+import static java.util.Collections.singletonList;
 import static org.mockito.Mockito.mock;
 import static org.mockito.Mockito.verify;
 import static org.mockito.Mockito.when;
@@ -74,12 +73,33 @@ public class XooBlameCommandTest {
       .setModuleBaseDir(baseDir.toPath())
       .build();
     fs.add(inputFile);
-
     BlameOutput result = mock(BlameOutput.class);
     when(input.filesToBlame()).thenReturn(Arrays.asList(inputFile));
+
     new XooBlameCommand().blame(input, result);
+
     verify(result).blameResult(inputFile, Arrays.asList(
       new BlameLine().revision("123").author("julien").date(DateUtils.parseDate("2014-12-12")),
       new BlameLine().revision("234").author("julien").date(DateUtils.parseDate("2014-12-24"))));
   }
+
+  @Test
+  public void blame_containing_author_with_comma() throws IOException {
+    File source = new File(baseDir, "src/foo.xoo");
+    FileUtils.write(source, "sample content");
+    File scm = new File(baseDir, "src/foo.xoo.scm");
+    FileUtils.write(scm, "\"123\",\"john,doe\",\"2019-01-22\"");
+    DefaultInputFile inputFile = new TestInputFileBuilder("foo", "src/foo.xoo")
+      .setLanguage(Xoo.KEY)
+      .setModuleBaseDir(baseDir.toPath())
+      .build();
+    fs.add(inputFile);
+    BlameOutput result = mock(BlameOutput.class);
+    when(input.filesToBlame()).thenReturn(Arrays.asList(inputFile));
+
+    new XooBlameCommand().blame(input, result);
+
+    verify(result).blameResult(inputFile, singletonList(
+      new BlameLine().revision("123").author("john,doe").date(DateUtils.parseDate("2019-01-22"))));
+  }
 }
index b646a926a4d8e1a107078a91f2b1646f40c1c305..631bc02d99895985dfaf7cccf5dfcd1702f0fb10 100644 (file)
@@ -98,6 +98,7 @@ import static org.sonar.server.es.BaseDoc.epochMillisToEpochSeconds;
 import static org.sonar.server.es.EsUtils.escapeSpecialRegexChars;
 import static org.sonar.server.issue.index.IssueIndex.Facet.ASSIGNED_TO_ME;
 import static org.sonar.server.issue.index.IssueIndex.Facet.ASSIGNEES;
+import static org.sonar.server.issue.index.IssueIndex.Facet.AUTHOR;
 import static org.sonar.server.issue.index.IssueIndex.Facet.AUTHORS;
 import static org.sonar.server.issue.index.IssueIndex.Facet.CREATED_AT;
 import static org.sonar.server.issue.index.IssueIndex.Facet.CWE;
@@ -147,9 +148,10 @@ import static org.sonar.server.issue.index.SecurityStandardHelper.SANS_TOP_25_IN
 import static org.sonar.server.issue.index.SecurityStandardHelper.SANS_TOP_25_POROUS_DEFENSES;
 import static org.sonar.server.issue.index.SecurityStandardHelper.SANS_TOP_25_RISKY_RESOURCE;
 import static org.sonar.server.issue.index.SecurityStandardHelper.UNKNOWN_STANDARD;
+import static org.sonarqube.ws.client.issue.IssuesWsParameters.DEPRECATED_PARAM_AUTHORS;
 import static org.sonarqube.ws.client.issue.IssuesWsParameters.FACET_MODE_EFFORT;
 import static org.sonarqube.ws.client.issue.IssuesWsParameters.PARAM_ASSIGNEES;
-import static org.sonarqube.ws.client.issue.IssuesWsParameters.PARAM_AUTHORS;
+import static org.sonarqube.ws.client.issue.IssuesWsParameters.PARAM_AUTHOR;
 import static org.sonarqube.ws.client.issue.IssuesWsParameters.PARAM_CREATED_AT;
 import static org.sonarqube.ws.client.issue.IssuesWsParameters.PARAM_CWE;
 import static org.sonarqube.ws.client.issue.IssuesWsParameters.PARAM_DIRECTORIES;
@@ -186,7 +188,8 @@ public class IssueIndex {
     LANGUAGES(PARAM_LANGUAGES, FIELD_ISSUE_LANGUAGE, MAX_FACET_SIZE),
     RULES(PARAM_RULES, FIELD_ISSUE_RULE_ID, MAX_FACET_SIZE),
     TAGS(PARAM_TAGS, FIELD_ISSUE_TAGS, MAX_FACET_SIZE),
-    AUTHORS(PARAM_AUTHORS, FIELD_ISSUE_AUTHOR_LOGIN, MAX_FACET_SIZE),
+    AUTHORS(DEPRECATED_PARAM_AUTHORS, FIELD_ISSUE_AUTHOR_LOGIN, MAX_FACET_SIZE),
+    AUTHOR(PARAM_AUTHOR, FIELD_ISSUE_AUTHOR_LOGIN, MAX_FACET_SIZE),
     PROJECT_UUIDS(FACET_PROJECTS, FIELD_ISSUE_PROJECT_UUID, MAX_FACET_SIZE),
     MODULE_UUIDS(PARAM_MODULE_UUIDS, FIELD_ISSUE_MODULE_UUID, MAX_FACET_SIZE),
     FILE_UUIDS(PARAM_FILE_UUIDS, FIELD_ISSUE_COMPONENT_UUID, MAX_FACET_SIZE),
@@ -570,6 +573,7 @@ public class IssueIndex {
       addSimpleStickyFacetIfNeeded(options, stickyFacetBuilder, esSearch, LANGUAGES, query.languages().toArray());
       addSimpleStickyFacetIfNeeded(options, stickyFacetBuilder, esSearch, RULES, query.rules().stream().map(RuleDefinitionDto::getId).toArray());
       addSimpleStickyFacetIfNeeded(options, stickyFacetBuilder, esSearch, AUTHORS, query.authors().toArray());
+      addSimpleStickyFacetIfNeeded(options, stickyFacetBuilder, esSearch, AUTHOR, query.authors().toArray());
       addSimpleStickyFacetIfNeeded(options, stickyFacetBuilder, esSearch, TAGS, query.tags().toArray());
       addSimpleStickyFacetIfNeeded(options, stickyFacetBuilder, esSearch, TYPES, query.types().toArray());
       addSimpleStickyFacetIfNeeded(options, stickyFacetBuilder, esSearch, OWASP_TOP_10, query.owaspTop10().toArray());
@@ -647,7 +651,7 @@ public class IssueIndex {
     if (Double.isInfinite(actualValue)) {
       return OptionalLong.empty();
     }
-    return OptionalLong.of((long)actualValue);
+    return OptionalLong.of((long) actualValue);
   }
 
   private void addAssignedToMeFacetIfNeeded(SearchRequestBuilder builder, SearchOptions options, IssueQuery query, Map<String, QueryBuilder> filters, QueryBuilder queryBuilder) {
index 523c462c4fc7afbc214d718338d23ff13d4b087c..326a3689ce4680ca6cbc621b9742d3f4ec0cbf45 100644 (file)
@@ -67,6 +67,7 @@ import static java.util.Collections.emptyList;
 import static java.util.Collections.singletonList;
 import static java.util.Optional.ofNullable;
 import static java.util.stream.Collectors.toList;
+import static org.sonar.api.server.ws.WebService.Param.FACETS;
 import static org.sonar.api.utils.Paging.forPageIndex;
 import static org.sonar.core.util.stream.MoreCollectors.toSet;
 import static org.sonar.process.ProcessProperties.Property.SONARCLOUD_ENABLED;
@@ -85,6 +86,7 @@ import static org.sonar.server.ws.KeyExamples.KEY_PULL_REQUEST_EXAMPLE_001;
 import static org.sonar.server.ws.WsUtils.writeProtobuf;
 import static org.sonarqube.ws.client.issue.IssuesWsParameters.ACTION_SEARCH;
 import static org.sonarqube.ws.client.issue.IssuesWsParameters.DEPRECATED_PARAM_ACTION_PLANS;
+import static org.sonarqube.ws.client.issue.IssuesWsParameters.DEPRECATED_PARAM_AUTHORS;
 import static org.sonarqube.ws.client.issue.IssuesWsParameters.FACET_MODE;
 import static org.sonarqube.ws.client.issue.IssuesWsParameters.FACET_MODE_COUNT;
 import static org.sonarqube.ws.client.issue.IssuesWsParameters.FACET_MODE_EFFORT;
@@ -92,7 +94,7 @@ import static org.sonarqube.ws.client.issue.IssuesWsParameters.PARAM_ADDITIONAL_
 import static org.sonarqube.ws.client.issue.IssuesWsParameters.PARAM_ASC;
 import static org.sonarqube.ws.client.issue.IssuesWsParameters.PARAM_ASSIGNED;
 import static org.sonarqube.ws.client.issue.IssuesWsParameters.PARAM_ASSIGNEES;
-import static org.sonarqube.ws.client.issue.IssuesWsParameters.PARAM_AUTHORS;
+import static org.sonarqube.ws.client.issue.IssuesWsParameters.PARAM_AUTHOR;
 import static org.sonarqube.ws.client.issue.IssuesWsParameters.PARAM_BRANCH;
 import static org.sonarqube.ws.client.issue.IssuesWsParameters.PARAM_COMPONENTS;
 import static org.sonarqube.ws.client.issue.IssuesWsParameters.PARAM_COMPONENT_KEYS;
@@ -142,7 +144,8 @@ public class SearchAction implements IssuesWsAction, Startable {
     PARAM_RULES,
     PARAM_ASSIGNEES,
     PARAM_REPORTERS,
-    PARAM_AUTHORS,
+    DEPRECATED_PARAM_AUTHORS,
+    PARAM_AUTHOR,
     PARAM_DIRECTORIES,
     PARAM_LANGUAGES,
     PARAM_TAGS,
@@ -190,7 +193,8 @@ public class SearchAction implements IssuesWsAction, Startable {
         PARAM_COMPONENT_KEYS, PARAM_COMPONENT_UUIDS, PARAM_COMPONENTS, PARAM_COMPONENT_ROOT_UUIDS, PARAM_COMPONENT_ROOTS)
       .setSince("3.6")
       .setChangelog(
-        new Change("7.6", String.format("The use of module keys in parameter '%s' is deprecated", PARAM_COMPONENT_KEYS)),
+        new Change("7.7", format("Value '%s' in parameter '%s' is deprecated, please use '%s' instead", DEPRECATED_PARAM_AUTHORS, FACETS, PARAM_AUTHOR)),
+        new Change("7.6", format("The use of module keys in parameter '%s' is deprecated", PARAM_COMPONENT_KEYS)),
         new Change("7.4", "The facet 'projectUuids' is dropped in favour of the new facet 'projects'. " +
           "Note that they are not strictly identical, the latter returns the project keys."),
         new Change("7.4", format("Parameter '%s' does not accept anymore deprecated value 'debt'", FACET_MODE)),
@@ -206,7 +210,7 @@ public class SearchAction implements IssuesWsAction, Startable {
       .setResponseExample(getClass().getResource("search-example.json"));
 
     action.addPagingParams(100, MAX_LIMIT);
-    action.createParam(Param.FACETS)
+    action.createParam(FACETS)
       .setDescription("Comma-separated list of the facets to be computed. No facet is computed by default.")
       .setPossibleValues(SUPPORTED_FACETS);
     action.createParam(FACET_MODE)
@@ -260,9 +264,13 @@ public class SearchAction implements IssuesWsAction, Startable {
     action.createParam(PARAM_CWE)
       .setDescription("Comma-separated list of CWE identifiers. Use '" + UNKNOWN_STANDARD + "' to select issues not associated to any CWE.")
       .setExampleValue("12,125," + UNKNOWN_STANDARD);
-    action.createParam(PARAM_AUTHORS)
-      .setDescription("Comma-separated list of SCM accounts")
+    action.createParam(DEPRECATED_PARAM_AUTHORS)
+      .setDeprecatedSince("7.7")
+      .setDescription("This parameter is deprecated, please use '%s' instead", PARAM_AUTHOR)
       .setExampleValue("torvalds@linux-foundation.org");
+    action.createParam(PARAM_AUTHOR)
+      .setDescription("SCM accounts. To set several values, the parameter must be called once for each value.")
+      .setExampleValue("author=torvalds@linux-foundation.org&author=linux@fondation.org");
     action.createParam(PARAM_ASSIGNEES)
       .setDescription("Comma-separated list of assignee logins. The value '__me__' can be used as a placeholder for user who performs the request")
       .setExampleValue("admin,usera,__me__");
@@ -448,7 +456,8 @@ public class SearchAction implements IssuesWsAction, Startable {
       if (!organizationDto.isPresent() || !userSession.hasMembership(organizationDto.get())) {
         // In order to display the authors facet, the organization parameter must be set and the user
         // must be member of this organization
-        requestedFacets.remove(PARAM_AUTHORS);
+        requestedFacets.remove(PARAM_AUTHOR);
+        requestedFacets.remove(DEPRECATED_PARAM_AUTHORS);
       }
     }
 
@@ -521,7 +530,7 @@ public class SearchAction implements IssuesWsAction, Startable {
       .setAsc(request.mandatoryParamAsBoolean(PARAM_ASC))
       .setAssigned(request.paramAsBoolean(PARAM_ASSIGNED))
       .setAssigneesUuid(getLogins(dbSession, request.paramAsStrings(PARAM_ASSIGNEES)))
-      .setAuthors(request.paramAsStrings(PARAM_AUTHORS))
+      .setAuthors(request.hasParam(PARAM_AUTHOR) ? request.multiParam(PARAM_AUTHOR) : request.paramAsStrings(DEPRECATED_PARAM_AUTHORS))
       .setComponentKeys(request.paramAsStrings(PARAM_COMPONENT_KEYS))
       .setComponentRootUuids(request.paramAsStrings(PARAM_COMPONENT_ROOT_UUIDS))
       .setComponentRoots(request.paramAsStrings(PARAM_COMPONENT_ROOTS))
@@ -533,7 +542,7 @@ public class SearchAction implements IssuesWsAction, Startable {
       .setCreatedInLast(request.param(PARAM_CREATED_IN_LAST))
       .setDirectories(request.paramAsStrings(PARAM_DIRECTORIES))
       .setFacetMode(request.mandatoryParam(FACET_MODE))
-      .setFacets(request.paramAsStrings(Param.FACETS))
+      .setFacets(request.paramAsStrings(FACETS))
       .setFileUuids(request.paramAsStrings(PARAM_FILE_UUIDS))
       .setIssues(request.paramAsStrings(PARAM_ISSUES))
       .setLanguages(request.paramAsStrings(PARAM_LANGUAGES))
index 216d3203bf5acf049989498bcadefc6450b737f9..bbf2838cea62e8a1c25d354fdaa71bad3c78afbc 100644 (file)
@@ -323,7 +323,21 @@ public class IssueIndexFacetsTest {
   }
 
   @Test
-  public void facets_on_authors() {
+  public void facets_on_author() {
+    ComponentDto project = newPrivateProjectDto(newOrganizationDto());
+    ComponentDto file = newFileDto(project, null);
+
+    indexIssues(
+      newDoc("I1", file).setAuthorLogin("steph"),
+      newDoc("I2", file).setAuthorLogin("marcel"),
+      newDoc("I3", file).setAuthorLogin("marcel"),
+      newDoc("I4", file).setAuthorLogin(null));
+
+    assertThatFacetHasOnly(IssueQuery.builder(), "author", entry("steph", 1L), entry("marcel", 2L));
+  }
+
+  @Test
+  public void facets_on_deprecated_authors() {
     ComponentDto project = newPrivateProjectDto(newOrganizationDto());
     ComponentDto file = newFileDto(project, null);
 
index f0bdcdbb37ac16bcbb20b8c3ead1e806dc4a786c..9b30a5b98a3853397b972ca380c72282571d3f64 100644 (file)
@@ -29,7 +29,6 @@ import org.sonar.api.config.internal.MapSettings;
 import org.sonar.api.resources.Languages;
 import org.sonar.api.resources.Qualifiers;
 import org.sonar.api.rule.RuleKey;
-import org.sonar.api.server.ws.WebService;
 import org.sonar.api.utils.Durations;
 import org.sonar.api.utils.System2;
 import org.sonar.db.DbClient;
@@ -38,7 +37,6 @@ import org.sonar.db.component.ComponentDto;
 import org.sonar.db.issue.IssueDto;
 import org.sonar.db.organization.OrganizationDto;
 import org.sonar.db.rule.RuleDefinitionDto;
-import org.sonar.db.user.UserDto;
 import org.sonar.server.es.EsTester;
 import org.sonar.server.issue.IssueFieldsSetter;
 import org.sonar.server.issue.TransitionService;
@@ -77,7 +75,6 @@ import static org.sonar.db.component.ComponentTesting.newSubView;
 import static org.sonar.db.component.ComponentTesting.newView;
 import static org.sonar.db.issue.IssueTesting.newIssue;
 import static org.sonarqube.ws.client.component.ComponentsWsParameters.PARAM_BRANCH;
-import static org.sonarqube.ws.client.issue.IssuesWsParameters.PARAM_AUTHORS;
 import static org.sonarqube.ws.client.issue.IssuesWsParameters.PARAM_COMPONENTS;
 import static org.sonarqube.ws.client.issue.IssuesWsParameters.PARAM_COMPONENT_KEYS;
 import static org.sonarqube.ws.client.issue.IssuesWsParameters.PARAM_COMPONENT_UUIDS;
@@ -434,32 +431,6 @@ public class SearchActionComponentsTest {
       .assertJson(this.getClass(), "no_issue.json");
   }
 
-  @Test
-  public void search_by_author() {
-    ComponentDto project = db.components().insertPublicProject(p -> p.setDbKey("PK1"));
-    ComponentDto file = db.components().insertComponent(newFileDto(project, null, "F1").setDbKey("FK1"));
-    RuleDefinitionDto rule = db.rules().insert(r -> r.setRuleKey(RuleKey.of("xoo", "x1")));
-    db.issues().insert(rule, project, file, i -> i.setAuthorLogin("leia").setKee("2bd4eac2-b650-4037-80bc-7b112bd4eac2"));
-    db.issues().insert(rule, project, file, i -> i.setAuthorLogin("luke@skywalker.name").setKee("82fd47d4-b650-4037-80bc-7b1182fd47d4"));
-    allowAnyoneOnProjects(project);
-    indexIssues();
-
-    UserDto user = db.users().insertUser();
-    db.organizations().addMember(db.getDefaultOrganization(), user);
-    userSession.logIn(user).addMembership(db.getDefaultOrganization());
-
-    ws.newRequest()
-      .setParam(PARAM_AUTHORS, "leia")
-      .setParam(WebService.Param.FACETS, "authors")
-      .execute()
-      .assertJson(this.getClass(), "search_by_authors.json");
-
-    ws.newRequest()
-      .setParam(PARAM_AUTHORS, "unknown")
-      .execute()
-      .assertJson(this.getClass(), "no_issue.json");
-  }
-
   @Test
   public void search_by_application_key() {
     ComponentDto application = db.components().insertPrivateApplication(db.getDefaultOrganization());
index 8027d014df66fa8184bce73eab0790cf615d1d44..2a5a338f4f0ab88edf44ce6892abfd452446847d 100644 (file)
@@ -76,6 +76,7 @@ import org.sonarqube.ws.Issues.Issue;
 import org.sonarqube.ws.Issues.SearchWsResponse;
 
 import static java.util.Arrays.asList;
+import static java.util.Collections.singletonList;
 import static org.assertj.core.api.Assertions.assertThat;
 import static org.assertj.core.groups.Tuple.tuple;
 import static org.junit.rules.ExpectedException.none;
@@ -163,7 +164,8 @@ public class SearchActionTest {
         Issue::getAssignee, Issue::getAuthor, Issue::getLine, Issue::getHash, Issue::getTagsList, Issue::getCreationDate, Issue::getUpdateDate)
       .containsExactlyInAnyOrder(
         tuple(organization.getKey(), issue.getKey(), rule.getKey().toString(), Severity.MAJOR, file.getKey(), RESOLUTION_FIXED, STATUS_RESOLVED, "the message", "10min",
-          simon.getLogin(), "John", 42, "a227e508d6646b55a086ee11d63b21e9", asList("bug", "owasp"), formatDateTime(issue.getIssueCreationDate()), formatDateTime(issue.getIssueUpdateDate())));
+          simon.getLogin(), "John", 42, "a227e508d6646b55a086ee11d63b21e9", asList("bug", "owasp"), formatDateTime(issue.getIssueCreationDate()),
+          formatDateTime(issue.getIssueUpdateDate())));
   }
 
   @Test
@@ -577,6 +579,72 @@ public class SearchActionTest {
       .assertJson(this.getClass(), "empty_result.json");
   }
 
+  @Test
+  public void search_by_author() {
+    ComponentDto project = db.components().insertPublicProject();
+    ComponentDto file = db.components().insertComponent(newFileDto(project, null));
+    RuleDefinitionDto rule = db.rules().insert();
+    IssueDto issue1 = db.issues().insert(rule, project, file, i -> i.setAuthorLogin("leia"));
+    IssueDto issue2 = db.issues().insert(rule, project, file, i -> i.setAuthorLogin("luke"));
+    IssueDto issue3 = db.issues().insert(rule, project, file, i -> i.setAuthorLogin("han, solo"));
+    indexPermissions();
+    indexIssues();
+
+    SearchWsResponse response = ws.newRequest()
+      .setMultiParam("author", asList("leia", "han, solo"))
+      .setParam(FACETS, "author")
+      .executeProtobuf(SearchWsResponse.class);
+    assertThat(response.getIssuesList())
+      .extracting(Issue::getKey)
+      .containsExactlyInAnyOrder(issue1.getKey(), issue3.getKey());
+    Common.Facet facet = response.getFacets().getFacetsList().get(0);
+    assertThat(facet.getProperty()).isEqualTo("author");
+    assertThat(facet.getValuesList())
+      .extracting(Common.FacetValue::getVal, Common.FacetValue::getCount)
+      .containsExactlyInAnyOrder(
+        tuple("leia", 1L),
+        tuple("luke", 1L),
+        tuple("han, solo", 1L));
+
+    assertThat(ws.newRequest()
+      .setMultiParam("author", singletonList("unknown"))
+      .executeProtobuf(SearchWsResponse.class).getIssuesList())
+        .isEmpty();
+  }
+
+  @Test
+  public void search_by_deprecated_authors_parameter() {
+    ComponentDto project = db.components().insertPublicProject();
+    ComponentDto file = db.components().insertComponent(newFileDto(project, null));
+    RuleDefinitionDto rule = db.rules().insert();
+    IssueDto issue1 = db.issues().insert(rule, project, file, i -> i.setAuthorLogin("leia"));
+    IssueDto issue2 = db.issues().insert(rule, project, file, i -> i.setAuthorLogin("luke"));
+    indexPermissions();
+    indexIssues();
+
+    SearchWsResponse response = ws.newRequest()
+      .setParam("authors", "leia")
+      .setParam(FACETS, "authors")
+      .executeProtobuf(SearchWsResponse.class);
+    assertThat(response.getIssuesList()).extracting(Issue::getKey).containsExactlyInAnyOrder(issue1.getKey());
+    Common.Facet facet = response.getFacets().getFacetsList().get(0);
+    assertThat(facet.getProperty()).isEqualTo("authors");
+    assertThat(facet.getValuesList())
+      .extracting(Common.FacetValue::getVal, Common.FacetValue::getCount)
+      .containsExactlyInAnyOrder(
+        tuple("leia", 1L),
+        tuple("luke", 1L));
+
+    // Deprecated parameter 'authors' will be ignored if new parameter 'author' is set
+    assertThat(ws.newRequest()
+      .setMultiParam("author", singletonList("luke"))
+      // This parameter will be ignored
+      .setParam("authors", "leia")
+      .executeProtobuf(SearchWsResponse.class).getIssuesList())
+        .extracting(Issue::getKey)
+        .containsExactlyInAnyOrder(issue2.getKey());
+  }
+
   @Test
   public void sort_by_updated_at() {
     RuleDto rule = newRule();
@@ -782,7 +850,7 @@ public class SearchActionTest {
     assertThat(def.responseExampleAsString()).isNotEmpty();
 
     assertThat(def.params()).extracting("key").containsExactlyInAnyOrder(
-      "additionalFields", "asc", "assigned", "assignees", "authors", "componentKeys", "componentRootUuids", "componentRoots", "componentUuids", "components", "branch",
+      "additionalFields", "asc", "assigned", "assignees", "authors", "author", "componentKeys", "componentRootUuids", "componentRoots", "componentUuids", "components", "branch",
       "pullRequest", "organization",
       "createdAfter", "createdAt", "createdBefore", "createdInLast", "directories", "facetMode", "facets", "fileUuids", "issues", "languages", "moduleUuids", "onComponentOnly",
       "p", "projects", "ps", "resolutions", "resolved", "rules", "s", "severities", "sinceLeakPeriod",
diff --git a/server/sonar-server/src/test/resources/org/sonar/server/issue/ws/SearchActionComponentsTest/search_by_authors.json b/server/sonar-server/src/test/resources/org/sonar/server/issue/ws/SearchActionComponentsTest/search_by_authors.json
deleted file mode 100644 (file)
index 4dbdc16..0000000
+++ /dev/null
@@ -1,28 +0,0 @@
-{
-  "total": 1,
-  "p": 1,
-  "issues": [
-    {
-      "key": "2bd4eac2-b650-4037-80bc-7b112bd4eac2",
-      "component": "FK1",
-      "project": "PK1",
-      "rule": "xoo:x1",
-      "author": "leia"
-    }
-  ],
-  "facets": [
-    {
-      "property": "authors",
-      "values": [
-        {
-          "val": "leia",
-          "count": 1
-        },
-        {
-          "val": "luke@skywalker.name",
-          "count": 1
-        }
-      ]
-    }
-  ]
-}
index 90bb5600ea64fdab9a5464567ee8fa3e9ebad25f..041813066b138d03068e579c1af3d9448b96de3f 100644 (file)
@@ -88,7 +88,14 @@ public class IssuesWsParameters {
   @Deprecated
   public static final String PARAM_REPORTERS = "reporters";
   public static final String PARAM_ASSIGNEES = "assignees";
-  public static final String PARAM_AUTHORS = "authors";
+
+  /**
+   * @deprecated since 7.7, please use 'author' instead
+   */
+  @Deprecated
+  public static final String DEPRECATED_PARAM_AUTHORS = "authors";
+
+  public static final String PARAM_AUTHOR = "author";
   public static final String PARAM_LANGUAGES = "languages";
   public static final String PARAM_TAGS = "tags";
   public static final String PARAM_TYPES = "types";
index 4a5ade2fa950a757df5d93b2364c500bbfbc5d46..bd898f434d99f9828d88fd8025cb0db96657e069 100644 (file)
@@ -147,7 +147,8 @@ public class IssuesService extends BaseService {
         .setParam("componentUuid", request.getComponentUuid())
         .setParam("createdAfter", request.getCreatedAfter())
         .setParam("ps", request.getPs())
-        .setMediaType(MediaTypes.JSON)).content();
+        .setMediaType(MediaTypes.JSON)
+      ).content();
   }
 
   /**
@@ -191,7 +192,8 @@ public class IssuesService extends BaseService {
       new PostRequest(path("edit_comment"))
         .setParam("comment", request.getComment())
         .setParam("text", request.getText())
-        .setMediaType(MediaTypes.JSON)).content();
+        .setMediaType(MediaTypes.JSON)
+      ).content();
   }
 
   /**
@@ -208,6 +210,7 @@ public class IssuesService extends BaseService {
         .setParam("asc", request.getAsc())
         .setParam("assigned", request.getAssigned())
         .setParam("assignees", request.getAssignees() == null ? null : request.getAssignees().stream().collect(Collectors.joining(",")))
+        .setParam("author", request.getAuthor())
         .setParam("authors", request.getAuthors() == null ? null : request.getAuthors().stream().collect(Collectors.joining(",")))
         .setParam("branch", request.getBranch())
         .setParam("componentKeys", request.getComponentKeys() == null ? null : request.getComponentKeys().stream().collect(Collectors.joining(",")))
@@ -219,6 +222,7 @@ public class IssuesService extends BaseService {
         .setParam("createdAt", request.getCreatedAt())
         .setParam("createdBefore", request.getCreatedBefore())
         .setParam("createdInLast", request.getCreatedInLast())
+        .setParam("cwe", request.getCwe() == null ? null : request.getCwe().stream().collect(Collectors.joining(",")))
         .setParam("directories", request.getDirectories() == null ? null : request.getDirectories().stream().collect(Collectors.joining(",")))
         .setParam("facetMode", request.getFacetMode())
         .setParam("facets", request.getFacets() == null ? null : request.getFacets().stream().collect(Collectors.joining(",")))
@@ -228,6 +232,7 @@ public class IssuesService extends BaseService {
         .setParam("moduleUuids", request.getModuleUuids() == null ? null : request.getModuleUuids().stream().collect(Collectors.joining(",")))
         .setParam("onComponentOnly", request.getOnComponentOnly())
         .setParam("organization", request.getOrganization())
+        .setParam("owaspTop10", request.getOwaspTop10() == null ? null : request.getOwaspTop10().stream().collect(Collectors.joining(",")))
         .setParam("p", request.getP())
         .setParam("projects", request.getProjects() == null ? null : request.getProjects().stream().collect(Collectors.joining(",")))
         .setParam("ps", request.getPs())
@@ -236,14 +241,12 @@ public class IssuesService extends BaseService {
         .setParam("resolved", request.getResolved())
         .setParam("rules", request.getRules() == null ? null : request.getRules().stream().collect(Collectors.joining(",")))
         .setParam("s", request.getS())
+        .setParam("sansTop25", request.getSansTop25() == null ? null : request.getSansTop25().stream().collect(Collectors.joining(",")))
         .setParam("severities", request.getSeverities() == null ? null : request.getSeverities().stream().collect(Collectors.joining(",")))
         .setParam("sinceLeakPeriod", request.getSinceLeakPeriod())
         .setParam("statuses", request.getStatuses() == null ? null : request.getStatuses().stream().collect(Collectors.joining(",")))
         .setParam("tags", request.getTags() == null ? null : request.getTags().stream().collect(Collectors.joining(",")))
-        .setParam("types", request.getTypes() == null ? null : request.getTypes().stream().collect(Collectors.joining(",")))
-        .setParam("owaspTop10", request.getOwaspTop10() == null ? null : request.getOwaspTop10().stream().collect(Collectors.joining(",")))
-        .setParam("sansTop25", request.getSansTop25() == null ? null : request.getSansTop25().stream().collect(Collectors.joining(",")))
-        .setParam("cwe", request.getCwe() == null ? null : request.getCwe().stream().collect(Collectors.joining(","))),
+        .setParam("types", request.getTypes() == null ? null : request.getTypes().stream().collect(Collectors.joining(","))),
       SearchWsResponse.parser());
   }
 
index 62906e70b7063cd573400d364ff012050edab6e7..08407bcda0ee57dc0b7fef2217800109c8d6c479 100644 (file)
@@ -35,6 +35,7 @@ public class SearchRequest {
   private String asc;
   private String assigned;
   private List<String> assignees;
+  private List<String> author;
   private List<String> authors;
   private String branch;
   private List<String> componentKeys;
@@ -142,9 +143,23 @@ public class SearchRequest {
     return assignees;
   }
 
+  /**
+   * Example value: "author=torvalds@linux-foundation.org&author=linux@fondation.org"
+   */
+  public SearchRequest setAuthor(List<String> author) {
+    this.author = author;
+    return this;
+  }
+
+  public List<String> getAuthor() {
+    return author;
+  }
+
   /**
    * Example value: "torvalds@linux-foundation.org"
+   * @deprecated since 7.7
    */
+  @Deprecated
   public SearchRequest setAuthors(List<String> authors) {
     this.authors = authors;
     return this;
@@ -310,7 +325,6 @@ public class SearchRequest {
    * <ul>
    *   <li>"count"</li>
    *   <li>"effort"</li>
-   *   <li>"debt"</li>
    * </ul>
    */
   public SearchRequest setFacetMode(String facetMode) {
@@ -325,18 +339,19 @@ public class SearchRequest {
   /**
    * Possible values:
    * <ul>
+   *   <li>"projects"</li>
+   *   <li>"moduleUuids"</li>
+   *   <li>"fileUuids"</li>
+   *   <li>"assigned_to_me"</li>
    *   <li>"severities"</li>
    *   <li>"statuses"</li>
    *   <li>"resolutions"</li>
    *   <li>"actionPlans"</li>
-   *   <li>"projectUuids"</li>
    *   <li>"rules"</li>
    *   <li>"assignees"</li>
-   *   <li>"assigned_to_me"</li>
    *   <li>"reporters"</li>
    *   <li>"authors"</li>
-   *   <li>"moduleUuids"</li>
-   *   <li>"fileUuids"</li>
+   *   <li>"author"</li>
    *   <li>"directories"</li>
    *   <li>"languages"</li>
    *   <li>"tags"</li>
@@ -396,7 +411,9 @@ public class SearchRequest {
   /**
    * This is part of the internal API.
    * Example value: "7d8749e8-3070-4903-9188-bdd82933bb92"
+   * @deprecated since 7.6
    */
+  @Deprecated
   public SearchRequest setModuleUuids(List<String> moduleUuids) {
     this.moduleUuids = moduleUuids;
     return this;