]> source.dussan.org Git - sonarqube.git/commitdiff
SONAR-4566 Search old projects in WS api/projects/search
authorTeryk Bellahsene <teryk.bellahsene@sonarsource.com>
Wed, 30 Aug 2017 13:46:54 +0000 (15:46 +0200)
committerStas Vilchik <stas.vilchik@sonarsource.com>
Mon, 11 Sep 2017 09:28:29 +0000 (11:28 +0200)
14 files changed:
server/sonar-db-dao/src/main/java/org/sonar/db/component/ComponentQuery.java
server/sonar-db-dao/src/main/resources/org/sonar/db/component/ComponentMapper.xml
server/sonar-db-dao/src/test/java/org/sonar/db/component/ComponentDaoTest.java
server/sonar-db-dao/src/test/java/org/sonar/db/component/ComponentQueryTest.java
server/sonar-server/src/main/java/org/sonar/server/project/ws/SearchAction.java
server/sonar-server/src/main/resources/org/sonar/server/project/ws/search-example.json
server/sonar-server/src/test/java/org/sonar/server/project/ws/SearchActionTest.java
sonar-ws/src/main/java/org/sonarqube/ws/client/project/ProjectsService.java
sonar-ws/src/main/java/org/sonarqube/ws/client/project/ProjectsWsParameters.java
sonar-ws/src/main/java/org/sonarqube/ws/client/project/SearchWsRequest.java
sonar-ws/src/main/protobuf/ws-projects.proto
sonar-ws/src/test/java/org/sonarqube/ws/client/project/ProjectsServiceTest.java
tests/src/test/java/org/sonarqube/tests/Category6Suite.java
tests/src/test/java/org/sonarqube/tests/projectAdministration/ProjectSearchTest.java [new file with mode: 0644]

index 72ed52dd99c18c007ac11d2c10bc3c2ca1e8adfd..58858246033bd5986990acf35975a0b3b064a624 100644 (file)
@@ -34,6 +34,7 @@ public class ComponentQuery {
   private final String language;
   private final Boolean isPrivate;
   private final Set<Long> componentIds;
+  private final Long analyzedBefore;
 
   /**
    * Used by Dev Cockpit 1.9.
@@ -48,6 +49,7 @@ public class ComponentQuery {
     this.language = null;
     this.componentIds = null;
     this.isPrivate = null;
+    this.analyzedBefore = null;
   }
 
   private ComponentQuery(Builder builder) {
@@ -56,6 +58,7 @@ public class ComponentQuery {
     this.language = builder.language;
     this.componentIds = builder.componentIds;
     this.isPrivate = builder.isPrivate;
+    this.analyzedBefore = builder.analyzedBefore;
   }
 
   public String[] getQualifiers() {
@@ -90,6 +93,11 @@ public class ComponentQuery {
     return isPrivate;
   }
 
+  @CheckForNull
+  public Long getAnalyzedBefore() {
+    return analyzedBefore;
+  }
+
   public static Builder builder() {
     return new Builder();
   }
@@ -100,6 +108,7 @@ public class ComponentQuery {
     private String language;
     private Boolean isPrivate;
     private Set<Long> componentIds;
+    private Long analyzedBefore;
 
     public Builder setNameOrKeyQuery(@Nullable String nameOrKeyQuery) {
       this.nameOrKeyQuery = nameOrKeyQuery;
@@ -126,6 +135,11 @@ public class ComponentQuery {
       return this;
     }
 
+    public Builder setAnalyzedBefore(@Nullable Long analyzedBefore) {
+      this.analyzedBefore = analyzedBefore;
+      return this;
+    }
+
     protected static String[] validateQualifiers(@Nullable String... qualifiers) {
       checkArgument(qualifiers != null && qualifiers.length > 0, "At least one qualifier must be provided");
       return qualifiers;
index 1c5a72ff24ccab97da04ae1027aa45f80d509ace..905d1ef8717e5e07707ecd3a049108df85fb8a93 100644 (file)
 
   <sql id="sqlSelectByQuery">
     from projects p
+    <if test="query.analyzedBefore!=null">
+      inner join snapshots s on s.component_uuid=p.uuid and s.status='P' and s.islast=${_true}
+        and s.created_at &lt; #{query.analyzedBefore,jdbcType=BIGINT}
+    </if>
     where
       p.enabled=${_true}
-      AND p.copy_component_uuid is null
+      and p.copy_component_uuid is null
       <if test="organizationUuid!=null">
         and p.organization_uuid=#{organizationUuid,jdbcType=VARCHAR}
       </if>
       <if test="query.qualifiers!=null">
-      AND p.qualifier in
-        <foreach collection="query.qualifiers" item="qualifier" open="(" close=")" separator=",">
-          #{qualifier,jdbcType=VARCHAR}
-        </foreach>
+        and p.qualifier in
+          <foreach collection="query.qualifiers" item="qualifier" open="(" close=")" separator=",">
+            #{qualifier,jdbcType=VARCHAR}
+          </foreach>
       </if>
       <if test="query.language!=null">
-        AND p.language = #{query.language,jdbcType=VARCHAR}
+        and p.language = #{query.language,jdbcType=VARCHAR}
       </if>
       <if test="query.componentIds!=null">
-        AND p.id in
+        and p.id in
         <foreach collection="query.componentIds" item="componentId" open="(" close=")" separator=",">
           #{componentId,jdbcType=BIGINT}
         </foreach>
index b63377d8427f7983c65aa6c5e534d2d94f3c1ac8..aae487bf389448b4bf7857ac63ec2191d527fb37 100644 (file)
@@ -58,6 +58,7 @@ import static org.sonar.db.component.ComponentTesting.newSubView;
 import static org.sonar.db.component.ComponentTesting.newView;
 import static org.sonar.db.component.ComponentTreeQuery.Strategy.CHILDREN;
 import static org.sonar.db.component.ComponentTreeQuery.Strategy.LEAVES;
+import static org.sonar.db.component.SnapshotTesting.newAnalysis;
 
 public class ComponentDaoTest {
 
@@ -984,6 +985,25 @@ public class ComponentDaoTest {
     assertThat(result.get(0).getDbKey()).isEqualTo("java-project-key");
   }
 
+  @Test
+  public void selectByQuery_filter_on_last_analysis_date() {
+    long aLongTimeAgo = 1_000_000_000L;
+    long recentTime = 3_000_000_000L;
+    ComponentDto oldProject = db.components().insertPublicProject();
+    db.getDbClient().snapshotDao().insert(dbSession, newAnalysis(oldProject).setCreatedAt(aLongTimeAgo));
+    ComponentDto recentProject = db.components().insertPublicProject();
+    db.getDbClient().snapshotDao().insert(dbSession, newAnalysis(recentProject).setCreatedAt(recentTime));
+    db.getDbClient().snapshotDao().insert(dbSession, newAnalysis(recentProject).setCreatedAt(aLongTimeAgo).setLast(false));
+    ComponentQuery.Builder query = ComponentQuery.builder().setQualifiers(Qualifiers.PROJECT);
+
+    assertThat(underTest.selectByQuery(dbSession, query.setAnalyzedBefore(recentTime).build(), 0, 10)).extracting(ComponentDto::getKey)
+      .containsExactlyInAnyOrder(oldProject.getKey());
+    assertThat(underTest.selectByQuery(dbSession, query.setAnalyzedBefore(aLongTimeAgo).build(), 0, 10)).extracting(ComponentDto::getKey)
+      .isEmpty();
+    assertThat(underTest.selectByQuery(dbSession, query.setAnalyzedBefore(recentTime + 1_000L).build(), 0, 10)).extracting(ComponentDto::getKey)
+      .containsExactlyInAnyOrder(oldProject.getKey(), recentProject.getKey());
+  }
+
   @Test
   public void selectByQuery_filter_on_visibility() {
     db.components().insertComponent(ComponentTesting.newPrivateProjectDto(db.getDefaultOrganization()).setDbKey("private-key"));
index 277cfcc862253115e61d539c559391c40fc51a1f..1b6971c6fc75045a9b4d976877557f6b5530284f 100644 (file)
@@ -36,12 +36,14 @@ public class ComponentQueryTest {
     ComponentQuery underTest = ComponentQuery.builder()
       .setNameOrKeyQuery("key")
       .setLanguage("java")
+      .setAnalyzedBefore(1_000_000_000L)
       .setQualifiers(PROJECT)
       .build();
 
     assertThat(underTest.getNameOrKeyQuery()).isEqualTo("key");
     assertThat(underTest.getLanguage()).isEqualTo("java");
     assertThat(underTest.getQualifiers()).containsOnly(PROJECT);
+    assertThat(underTest.getAnalyzedBefore()).isEqualTo(1_000_000_000L);
   }
 
   @Test
index e801645689c2a4a3256a8ae03e250a9609302f12..9aed6b6e00f57351f734fe3ae841c99bfe11591b 100644 (file)
 package org.sonar.server.project.ws;
 
 import java.util.List;
+import java.util.Map;
+import javax.annotation.Nullable;
 import org.sonar.api.server.ws.Change;
 import org.sonar.api.server.ws.Request;
 import org.sonar.api.server.ws.Response;
 import org.sonar.api.server.ws.WebService;
 import org.sonar.api.server.ws.WebService.Param;
 import org.sonar.api.utils.Paging;
+import org.sonar.core.util.stream.MoreCollectors;
 import org.sonar.db.DbClient;
 import org.sonar.db.DbSession;
 import org.sonar.db.component.ComponentDto;
 import org.sonar.db.component.ComponentQuery;
+import org.sonar.db.component.SnapshotDto;
 import org.sonar.db.organization.OrganizationDto;
 import org.sonar.db.permission.OrganizationPermission;
 import org.sonar.server.organization.DefaultOrganizationProvider;
@@ -43,6 +47,8 @@ import static java.util.Optional.ofNullable;
 import static org.sonar.api.resources.Qualifiers.APP;
 import static org.sonar.api.resources.Qualifiers.PROJECT;
 import static org.sonar.api.resources.Qualifiers.VIEW;
+import static org.sonar.api.utils.DateUtils.formatDateTime;
+import static org.sonar.api.utils.DateUtils.parseDateOrDateTime;
 import static org.sonar.core.util.Protobuf.setNullable;
 import static org.sonar.server.project.Visibility.PRIVATE;
 import static org.sonar.server.project.Visibility.PUBLIC;
@@ -51,6 +57,7 @@ import static org.sonarqube.ws.WsProjects.SearchWsResponse.Component;
 import static org.sonarqube.ws.WsProjects.SearchWsResponse.newBuilder;
 import static org.sonarqube.ws.client.project.ProjectsWsParameters.ACTION_SEARCH;
 import static org.sonarqube.ws.client.project.ProjectsWsParameters.MAX_PAGE_SIZE;
+import static org.sonarqube.ws.client.project.ProjectsWsParameters.PARAM_ANALYZED_BEFORE;
 import static org.sonarqube.ws.client.project.ProjectsWsParameters.PARAM_ORGANIZATION;
 import static org.sonarqube.ws.client.project.ProjectsWsParameters.PARAM_QUALIFIERS;
 import static org.sonarqube.ws.client.project.ProjectsWsParameters.PARAM_VISIBILITY;
@@ -97,6 +104,11 @@ public class SearchAction implements ProjectsWsAction {
       .setInternal(true)
       .setSince("6.4")
       .setPossibleValues(Visibility.getLabels());
+
+    action.createParam(PARAM_ANALYZED_BEFORE)
+      .setDescription("Filter the projects for which last analysis is older than the given date (exclusive).<br> " +
+        "Format: date or datetime ISO formats.")
+      .setSince("6.6");
   }
 
   @Override
@@ -113,6 +125,7 @@ public class SearchAction implements ProjectsWsAction {
       .setPage(request.mandatoryParamAsInt(Param.PAGE))
       .setPageSize(request.mandatoryParamAsInt(Param.PAGE_SIZE))
       .setVisibility(request.param(PARAM_VISIBILITY))
+      .setAnalyzedBefore(request.param(PARAM_ANALYZED_BEFORE))
       .build();
   }
 
@@ -124,7 +137,10 @@ public class SearchAction implements ProjectsWsAction {
       ComponentQuery query = buildQuery(request);
       Paging paging = buildPaging(dbSession, request, organization, query);
       List<ComponentDto> components = dbClient.componentDao().selectByQuery(dbSession, organization.getUuid(), query, paging.offset(), paging.pageSize());
-      return buildResponse(components, organization, paging);
+      Map<String, Long> analysisDateByComponentUuid = dbClient.snapshotDao()
+        .selectLastAnalysesByRootComponentUuids(dbSession, components.stream().map(ComponentDto::uuid).collect(MoreCollectors.toList())).stream()
+        .collect(MoreCollectors.uniqueIndex(SnapshotDto::getComponentUuid, SnapshotDto::getCreatedAt));
+      return buildResponse(components, organization, analysisDateByComponentUuid, paging);
     }
   }
 
@@ -135,6 +151,7 @@ public class SearchAction implements ProjectsWsAction {
       .setQualifiers(qualifiers.toArray(new String[qualifiers.size()]));
 
     setNullable(request.getVisibility(), v -> query.setPrivate(Visibility.isPrivate(v)));
+    setNullable(request.getAnalyzedBefore(), d -> query.setAnalyzedBefore(parseDateOrDateTime(d).getTime()));
 
     return query.build();
   }
@@ -146,7 +163,7 @@ public class SearchAction implements ProjectsWsAction {
       .andTotal(total);
   }
 
-  private static SearchWsResponse buildResponse(List<ComponentDto> components, OrganizationDto organization, Paging paging) {
+  private static SearchWsResponse buildResponse(List<ComponentDto> components, OrganizationDto organization, Map<String, Long> analysisDateByComponentUuid, Paging paging) {
     SearchWsResponse.Builder responseBuilder = newBuilder();
     responseBuilder.getPagingBuilder()
       .setPageIndex(paging.pageIndex())
@@ -155,12 +172,12 @@ public class SearchAction implements ProjectsWsAction {
       .build();
 
     components.stream()
-      .map(dto -> dtoToProject(organization, dto))
+      .map(dto -> dtoToProject(organization, dto, analysisDateByComponentUuid.get(dto.uuid())))
       .forEach(responseBuilder::addComponents);
     return responseBuilder.build();
   }
 
-  private static Component dtoToProject(OrganizationDto organization, ComponentDto dto) {
+  private static Component dtoToProject(OrganizationDto organization, ComponentDto dto, @Nullable Long analysisDate) {
     checkArgument(
       organization.getUuid().equals(dto.getOrganizationUuid()),
       "No Organization found for uuid '%s'",
@@ -173,6 +190,8 @@ public class SearchAction implements ProjectsWsAction {
       .setName(dto.name())
       .setQualifier(dto.qualifier())
       .setVisibility(dto.isPrivate() ? PRIVATE.getLabel() : PUBLIC.getLabel());
+    setNullable(analysisDate, d -> builder.setLastAnalysisDate(formatDateTime(d)));
+
     return builder.build();
   }
 
index dd9c8a510c2a7650d470be4407ae928562d7ac6a..ac6accbe924773294b639efd6838e1156685bc1f 100644 (file)
@@ -11,7 +11,8 @@
       "key": "project-key-1",
       "name": "Project Name 1",
       "qualifier": "TRK",
-      "visibility": "public"
+      "visibility": "public",
+      "lastAnalysisDate": "2017-03-01T11:39:03+0300"
     },
     {
       "organization": "my-org-1",
@@ -19,7 +20,8 @@
       "key": "project-key-2",
       "name": "Project Name 1",
       "qualifier": "TRK",
-      "visibility": "private"
+      "visibility": "private",
+      "lastAnalysisDate": "2017-03-02T15:21:47+0300"
     }
   ]
 }
index 356e9cf9033d6333e10e6893fb97a81ae5a3cc95..c6739877a71634ff2e73ae08cc28076a9d37e623 100644 (file)
@@ -23,6 +23,7 @@ import com.google.common.base.Joiner;
 import java.io.IOException;
 import java.net.URISyntaxException;
 import java.util.ArrayList;
+import java.util.Date;
 import java.util.List;
 import org.assertj.core.api.Assertions;
 import org.junit.Rule;
@@ -54,15 +55,19 @@ import static org.mockito.Mockito.mock;
 import static org.sonar.api.server.ws.WebService.Param.PAGE;
 import static org.sonar.api.server.ws.WebService.Param.PAGE_SIZE;
 import static org.sonar.api.server.ws.WebService.Param.TEXT_QUERY;
+import static org.sonar.api.utils.DateUtils.formatDate;
+import static org.sonar.api.utils.DateUtils.parseDateTime;
 import static org.sonar.core.util.Protobuf.setNullable;
 import static org.sonar.db.component.ComponentTesting.newDirectory;
 import static org.sonar.db.component.ComponentTesting.newFileDto;
 import static org.sonar.db.component.ComponentTesting.newModuleDto;
 import static org.sonar.db.component.ComponentTesting.newPrivateProjectDto;
 import static org.sonar.db.component.ComponentTesting.newView;
+import static org.sonar.db.component.SnapshotTesting.newAnalysis;
 import static org.sonar.db.permission.OrganizationPermission.ADMINISTER;
 import static org.sonar.db.permission.OrganizationPermission.ADMINISTER_QUALITY_PROFILES;
 import static org.sonar.test.JsonAssert.assertJson;
+import static org.sonarqube.ws.client.project.ProjectsWsParameters.PARAM_ANALYZED_BEFORE;
 import static org.sonarqube.ws.client.project.ProjectsWsParameters.PARAM_ORGANIZATION;
 import static org.sonarqube.ws.client.project.ProjectsWsParameters.PARAM_VISIBILITY;
 
@@ -204,6 +209,24 @@ public class SearchActionTest {
     assertThat(response.getComponentsList()).extracting(Component::getKey).containsOnly(project1.getDbKey(), project2.getDbKey());
   }
 
+  @Test
+  public void search_for_old_projects() {
+    userSession.addPermission(ADMINISTER, db.getDefaultOrganization());
+    long aLongTimeAgo = 1_000_000_000L;
+    long recentTime = 3_000_000_000L;
+    ComponentDto oldProject = db.components().insertPublicProject();
+    db.getDbClient().snapshotDao().insert(db.getSession(), newAnalysis(oldProject).setCreatedAt(aLongTimeAgo));
+    ComponentDto recentProject = db.components().insertPublicProject();
+    db.getDbClient().snapshotDao().insert(db.getSession(), newAnalysis(recentProject).setCreatedAt(recentTime));
+    db.commit();
+
+    SearchWsResponse result = call(SearchWsRequest.builder().setAnalyzedBefore(formatDate(new Date(recentTime))).build());
+
+    assertThat(result.getComponentsList()).extracting(Component::getKey)
+      .containsExactlyInAnyOrder(oldProject.getKey())
+      .doesNotContain(recentProject.getKey());
+  }
+
   @Test
   public void result_is_paginated() throws IOException {
     userSession.addPermission(ADMINISTER, db.getDefaultOrganization());
@@ -243,7 +266,7 @@ public class SearchActionTest {
   }
 
   @Test
-  public void verify_define() {
+  public void definition() {
     WebService.Action action = ws.getDef();
     assertThat(action.key()).isEqualTo("search");
     assertThat(action.isPost()).isFalse();
@@ -251,7 +274,7 @@ public class SearchActionTest {
     assertThat(action.isInternal()).isTrue();
     assertThat(action.since()).isEqualTo("6.3");
     assertThat(action.handler()).isEqualTo(ws.getDef().handler());
-    assertThat(action.params()).hasSize(6);
+    assertThat(action.params()).hasSize(7);
     assertThat(action.responseExample()).isEqualTo(getClass().getResource("search-example.json"));
 
     WebService.Param organization = action.param("organization");
@@ -284,21 +307,32 @@ public class SearchActionTest {
     assertThat(visibilityParam.isRequired()).isFalse();
     assertThat(visibilityParam.description()).isEqualTo("Filter the projects that should be visible to everyone (public), or only specific user/groups (private).<br/>" +
       "If no visibility is specified, the default project visibility of the organization will be used.");
+
+    WebService.Param lastAnalysisBefore = action.param("analyzedBefore");
+    assertThat(lastAnalysisBefore.isRequired()).isFalse();
+    assertThat(lastAnalysisBefore.since()).isEqualTo("6.6");
   }
 
   @Test
-  public void verify_response_example() throws URISyntaxException, IOException {
+  public void json_example() throws URISyntaxException, IOException {
     OrganizationDto organization = db.organizations().insertForKey("my-org-1");
     userSession.addPermission(ADMINISTER, organization);
+    ComponentDto publicProject = newPrivateProjectDto(organization, "project-uuid-1").setName("Project Name 1").setDbKey("project-key-1").setPrivate(false);
+    ComponentDto privateProject = newPrivateProjectDto(organization, "project-uuid-2").setName("Project Name 1").setDbKey("project-key-2");
     db.components().insertComponents(
-      newPrivateProjectDto(organization, "project-uuid-1").setName("Project Name 1").setDbKey("project-key-1").setPrivate(false),
-      newPrivateProjectDto(organization, "project-uuid-2").setName("Project Name 1").setDbKey("project-key-2"));
+      publicProject,
+      privateProject);
+    db.getDbClient().snapshotDao().insert(db.getSession(), newAnalysis(publicProject).setCreatedAt(parseDateTime("2017-03-01T11:39:03+0300").getTime()));
+    db.getDbClient().snapshotDao().insert(db.getSession(), newAnalysis(privateProject).setCreatedAt(parseDateTime("2017-03-02T15:21:47+0300").getTime()));
+    db.commit();
 
     String response = ws.newRequest()
       .setMediaType(MediaTypes.JSON)
       .setParam(PARAM_ORGANIZATION, organization.getKey())
       .execute().getInput();
+
     assertJson(response).isSimilarTo(ws.getDef().responseExampleAsString());
+    assertJson(ws.getDef().responseExampleAsString()).isSimilarTo(response);
   }
 
   private SearchWsResponse call(SearchWsRequest wsRequest) {
@@ -312,6 +346,7 @@ public class SearchActionTest {
     setNullable(wsRequest.getPage(), page -> request.setParam(PAGE, String.valueOf(page)));
     setNullable(wsRequest.getPageSize(), pageSize -> request.setParam(PAGE_SIZE, String.valueOf(pageSize)));
     setNullable(wsRequest.getVisibility(), v -> request.setParam(PARAM_VISIBILITY, v));
+    setNullable(wsRequest.getAnalyzedBefore(), d -> request.setParam(PARAM_ANALYZED_BEFORE, d));
     return request.executeProtobuf(SearchWsResponse.class);
   }
 
index f43c40d50be254e973846ddcc431c7e2ae1a1678..5dde5e0939fe8079f4ef401593590105cb784b80 100644 (file)
@@ -31,12 +31,14 @@ import org.sonarqube.ws.client.WsConnector;
 import static org.sonar.api.server.ws.WebService.Param.PAGE;
 import static org.sonar.api.server.ws.WebService.Param.PAGE_SIZE;
 import static org.sonar.api.server.ws.WebService.Param.TEXT_QUERY;
+import static org.sonar.api.utils.DateUtils.formatDateTimeNullSafe;
 import static org.sonarqube.ws.client.project.ProjectsWsParameters.ACTION_BULK_UPDATE_KEY;
 import static org.sonarqube.ws.client.project.ProjectsWsParameters.ACTION_CREATE;
 import static org.sonarqube.ws.client.project.ProjectsWsParameters.ACTION_SEARCH;
 import static org.sonarqube.ws.client.project.ProjectsWsParameters.ACTION_UPDATE_KEY;
 import static org.sonarqube.ws.client.project.ProjectsWsParameters.ACTION_UPDATE_VISIBILITY;
 import static org.sonarqube.ws.client.project.ProjectsWsParameters.CONTROLLER;
+import static org.sonarqube.ws.client.project.ProjectsWsParameters.PARAM_ANALYZED_BEFORE;
 import static org.sonarqube.ws.client.project.ProjectsWsParameters.PARAM_BRANCH;
 import static org.sonarqube.ws.client.project.ProjectsWsParameters.PARAM_FROM;
 import static org.sonarqube.ws.client.project.ProjectsWsParameters.PARAM_NAME;
@@ -112,6 +114,7 @@ public class ProjectsService extends BaseService {
     GetRequest get = new GetRequest(path(ACTION_SEARCH))
       .setParam(PARAM_ORGANIZATION, request.getOrganization())
       .setParam(PARAM_QUALIFIERS, Joiner.on(",").join(request.getQualifiers()))
+      .setParam(PARAM_ANALYZED_BEFORE, request.getAnalyzedBefore())
       .setParam(TEXT_QUERY, request.getQuery())
       .setParam(PAGE, request.getPage())
       .setParam(PAGE_SIZE, request.getPageSize());
index f4e9c7bc438cffadcb8d565fa0365dadb5fcad51..e03d82faa64124722065a505e84abbc2479ccab7 100644 (file)
@@ -41,8 +41,9 @@ public class ProjectsWsParameters {
   public static final String PARAM_FROM = "from";
   public static final String PARAM_TO = "to";
   public static final String PARAM_DRY_RUN = "dryRun";
-
   public static final String PARAM_VISIBILITY = "visibility";
+  public static final String PARAM_ANALYZED_BEFORE = "analyzedBefore";
+
   public static final String FILTER_LANGUAGES = "languages";
   public static final String FILTER_TAGS = "tags";
 
index 2b0be938a082fd93818af9b6d61d164b7b0e5876..ef36c34b61dd7e14c346f5f4bcee08d5de1b83b7 100644 (file)
@@ -36,6 +36,7 @@ public class SearchWsRequest {
   private final String visibility;
   private final Integer page;
   private final Integer pageSize;
+  private final String analyzedBefore;
 
   public SearchWsRequest(Builder builder) {
     this.organization = builder.organization;
@@ -44,6 +45,7 @@ public class SearchWsRequest {
     this.visibility = builder.visibility;
     this.page = builder.page;
     this.pageSize = builder.pageSize;
+    this.analyzedBefore = builder.analyzedBefore;
   }
 
   @CheckForNull
@@ -75,6 +77,11 @@ public class SearchWsRequest {
     return visibility;
   }
 
+  @CheckForNull
+  public String getAnalyzedBefore() {
+    return analyzedBefore;
+  }
+
   public static Builder builder() {
     return new Builder();
   }
@@ -86,6 +93,7 @@ public class SearchWsRequest {
     private Integer pageSize;
     private String query;
     private String visibility;
+    private String analyzedBefore;
 
     public Builder setOrganization(@Nullable String organization) {
       this.organization = organization;
@@ -117,10 +125,14 @@ public class SearchWsRequest {
       return this;
     }
 
+    public Builder setAnalyzedBefore(@Nullable String lastAnalysisBefore) {
+      this.analyzedBefore = lastAnalysisBefore;
+      return this;
+    }
+
     public SearchWsRequest build() {
       checkArgument(pageSize == null || pageSize <= MAX_PAGE_SIZE, "Page size must not be greater than %s", MAX_PAGE_SIZE);
       return new SearchWsRequest(this);
     }
   }
-
 }
index 0ec1cbad85cec37e9f09f66314ca0594bb0db95b..8668ffc355a34fd08683c6da23ea5a19267e644c 100644 (file)
@@ -70,6 +70,7 @@ message SearchWsResponse {
     optional string name = 4;
     optional string qualifier = 5;
     optional string visibility = 6;
+    optional string lastAnalysisDate = 7;
   }
 }
 
index 8d21940ab027a878600db9a1452c74cac8a01e37..d8b8c7e5e6f117321831928f37f0516afc65f607 100644 (file)
@@ -138,6 +138,7 @@ public class ProjectsServiceTest {
       .setOrganization("default")
       .setQuery("project")
       .setQualifiers(asList("TRK", "VW"))
+      .setAnalyzedBefore("2017-09-01")
       .setPage(3)
       .setPageSize(10)
       .build());
@@ -146,6 +147,7 @@ public class ProjectsServiceTest {
       .hasPath("search")
       .hasParam("organization", "default")
       .hasParam("q", "project")
+      .hasParam("analyzedBefore", "2017-09-01")
       .hasParam("qualifiers", "TRK,VW")
       .hasParam(PAGE, 3)
       .hasParam(PAGE_SIZE, 10)
index 01e7db07d077269b874baf0f238cbfa604c8fde4..d075f20959660539a238f8409a2e4734b699382d 100644 (file)
@@ -38,6 +38,7 @@ import org.sonarqube.tests.organization.RootUserOnOrganizationTest;
 import org.sonarqube.tests.projectAdministration.ProjectDeletionTest;
 import org.sonarqube.tests.projectAdministration.ProjectKeyUpdateTest;
 import org.sonarqube.tests.projectAdministration.ProjectProvisioningTest;
+import org.sonarqube.tests.projectAdministration.ProjectSearchTest;
 import org.sonarqube.tests.projectSearch.LeakProjectsPageTest;
 import org.sonarqube.tests.projectSearch.SearchProjectsTest;
 import org.sonarqube.tests.qualityGate.OrganizationQualityGateUiTest;
@@ -79,6 +80,7 @@ import static util.ItUtils.xooPlugin;
   ProjectDeletionTest.class,
   ProjectProvisioningTest.class,
   ProjectKeyUpdateTest.class,
+  ProjectSearchTest.class,
   PermissionTemplateTest.class
 })
 public class Category6Suite {
diff --git a/tests/src/test/java/org/sonarqube/tests/projectAdministration/ProjectSearchTest.java b/tests/src/test/java/org/sonarqube/tests/projectAdministration/ProjectSearchTest.java
new file mode 100644 (file)
index 0000000..6917828
--- /dev/null
@@ -0,0 +1,76 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2017 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program 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.
+ *
+ * This program 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.sonarqube.tests.projectAdministration;
+
+import com.sonar.orchestrator.Orchestrator;
+import java.util.Date;
+import org.apache.commons.lang.time.DateUtils;
+import org.junit.ClassRule;
+import org.junit.Rule;
+import org.junit.Test;
+import org.sonarqube.tests.Category6Suite;
+import org.sonarqube.tests.Tester;
+import org.sonarqube.ws.Organizations;
+import org.sonarqube.ws.WsProjects.CreateWsResponse;
+import org.sonarqube.ws.WsProjects.SearchWsResponse;
+import org.sonarqube.ws.client.project.SearchWsRequest;
+
+import static java.util.Collections.singletonList;
+import static org.assertj.core.api.Assertions.assertThat;
+import static util.ItUtils.formatDate;
+import static util.ItUtils.runProjectAnalysis;
+
+public class ProjectSearchTest {
+
+  @ClassRule
+  public static Orchestrator orchestrator = Category6Suite.ORCHESTRATOR;
+  @Rule
+  public Tester tester = new Tester(orchestrator);
+
+  @Test
+  public void search_old_projects() {
+    Organizations.Organization organization = tester.organizations().generate();
+    CreateWsResponse.Project oldProject = tester.projects().generate(organization);
+    CreateWsResponse.Project recentProject = tester.projects().generate(organization);
+    Date now = new Date();
+    Date oneYearAgo = DateUtils.addDays(now, -365);
+    Date moreThanOneYearAgo = DateUtils.addDays(now, -366);
+
+    analyzeProject(oldProject.getKey(), moreThanOneYearAgo, organization.getKey());
+    analyzeProject(recentProject.getKey(), now, organization.getKey());
+
+    SearchWsResponse result = tester.wsClient().projects().search(SearchWsRequest.builder()
+      .setOrganization(organization.getKey())
+      .setQualifiers(singletonList("TRK"))
+      .setAnalyzedBefore(formatDate(oneYearAgo)).build());
+
+    assertThat(result.getComponentsList()).extracting(SearchWsResponse.Component::getKey).containsExactlyInAnyOrder(oldProject.getKey());
+  }
+
+  private void analyzeProject(String projectKey, Date analysisDate, String organizationKey) {
+    runProjectAnalysis(orchestrator, "shared/xoo-sample",
+      "sonar.organization", organizationKey,
+      "sonar.projectKey", projectKey,
+      "sonar.projectDate", formatDate(analysisDate),
+      "sonar.login", "admin",
+      "sonar.password", "admin");
+  }
+}