]> source.dussan.org Git - sonarqube.git/commitdiff
SONAR-8427 Add a timeZone param to api/issues/search
authorJulien HENRY <julien.henry@sonarsource.com>
Thu, 3 Dec 2020 08:37:58 +0000 (09:37 +0100)
committersonartech <sonartech@sonarsource.com>
Thu, 3 Dec 2020 20:06:38 +0000 (20:06 +0000)
25 files changed:
server/sonar-server-common/src/main/java/org/sonar/server/es/Facets.java
server/sonar-server-common/src/main/java/org/sonar/server/es/SearchIdResult.java
server/sonar-server-common/src/main/java/org/sonar/server/es/SearchResult.java
server/sonar-server-common/src/main/java/org/sonar/server/issue/SearchRequest.java
server/sonar-server-common/src/main/java/org/sonar/server/rule/index/RuleIndex.java
server/sonar-server-common/src/main/java/org/sonar/server/user/index/UserIndex.java
server/sonar-web/src/main/js/apps/issues/components/AppContainer.tsx
server/sonar-webserver-core/src/test/java/org/sonar/server/rule/RegisterRulesTest.java
server/sonar-webserver-es/src/main/java/org/sonar/server/component/index/ComponentIndex.java
server/sonar-webserver-es/src/main/java/org/sonar/server/issue/index/IssueIndex.java
server/sonar-webserver-es/src/main/java/org/sonar/server/issue/index/IssueQuery.java
server/sonar-webserver-es/src/main/java/org/sonar/server/issue/index/IssueQueryFactory.java
server/sonar-webserver-es/src/main/java/org/sonar/server/measure/index/ProjectMeasuresIndex.java
server/sonar-webserver-es/src/test/java/org/sonar/server/issue/index/IssueIndexDebtTest.java
server/sonar-webserver-es/src/test/java/org/sonar/server/issue/index/IssueIndexFacetsTest.java
server/sonar-webserver-es/src/test/java/org/sonar/server/issue/index/IssueIndexSecurityHotspotsTest.java
server/sonar-webserver-es/src/test/java/org/sonar/server/issue/index/IssueQueryFactoryTest.java
server/sonar-webserver-webapi/src/main/java/org/sonar/server/issue/ws/SearchAction.java
server/sonar-webserver-webapi/src/test/java/org/sonar/server/issue/ws/SearchActionTest.java
server/sonar-webserver-webapi/src/test/java/org/sonar/server/rule/RuleCreatorTest.java
server/sonar-webserver-webapi/src/test/java/org/sonar/server/rule/RuleUpdaterTest.java
sonar-plugin-api-impl/src/main/java/org/sonar/api/impl/utils/TestSystem2.java
sonar-plugin-api/src/main/java/org/sonar/api/utils/DateUtils.java
sonar-plugin-api/src/test/java/org/sonar/api/utils/DateUtilsTest.java
sonar-ws/src/main/java/org/sonarqube/ws/client/issue/IssuesWsParameters.java

index 27e7d089ae8aa1da42567bde2dcddca67d9cf72d..02b6b7e93442b741fa34eaa93f09961e81be3774 100644 (file)
  */
 package org.sonar.server.es;
 
+import java.time.ZoneId;
 import java.util.Collections;
 import java.util.Date;
 import java.util.LinkedHashMap;
 import java.util.List;
 import java.util.Map;
 import java.util.Set;
-import java.util.TimeZone;
 import javax.annotation.CheckForNull;
 import org.apache.commons.lang.builder.ReflectionToStringBuilder;
 import org.apache.commons.lang.builder.ToStringStyle;
@@ -50,14 +50,14 @@ public class Facets {
   private static final java.lang.String NO_DATA_PREFIX = "no_data_";
 
   private final LinkedHashMap<String, LinkedHashMap<String, Long>> facetsByName;
-  private final TimeZone timeZone;
+  private final ZoneId timeZone;
 
-  public Facets(LinkedHashMap<String, LinkedHashMap<String, Long>> facetsByName, TimeZone timeZone) {
+  public Facets(LinkedHashMap<String, LinkedHashMap<String, Long>> facetsByName, ZoneId timeZone) {
     this.facetsByName = facetsByName;
     this.timeZone = timeZone;
   }
 
-  public Facets(SearchResponse response, TimeZone timeZone) {
+  public Facets(SearchResponse response, ZoneId timeZone) {
     this.facetsByName = new LinkedHashMap<>();
     this.timeZone = timeZone;
     Aggregations aggregations = response.getAggregations();
@@ -144,9 +144,9 @@ public class Facets {
     }
   }
 
-  private static String dateTimeToDate(String timestamp, TimeZone timeZone) {
+  private static String dateTimeToDate(String timestamp, ZoneId timeZone) {
     Date date = parseDateTime(timestamp);
-    return date.toInstant().atZone(timeZone.toZoneId()).toLocalDate().toString();
+    return date.toInstant().atZone(timeZone).toLocalDate().toString();
   }
 
   private void processSum(Sum aggregation) {
index c369f0759a92846a6c7c499d8375df2a7ca85d92..6783537663fc67dd6008458280e6677a000da0a6 100644 (file)
@@ -20,9 +20,9 @@
 package org.sonar.server.es;
 
 import com.google.common.base.Function;
+import java.time.ZoneId;
 import java.util.ArrayList;
 import java.util.List;
-import java.util.TimeZone;
 import org.apache.commons.lang.builder.ReflectionToStringBuilder;
 import org.elasticsearch.action.search.SearchResponse;
 import org.elasticsearch.search.SearchHit;
@@ -34,7 +34,7 @@ public class SearchIdResult<ID> {
   private final Facets facets;
   private final long total;
 
-  public SearchIdResult(SearchResponse response, Function<String, ID> converter, TimeZone timeZone) {
+  public SearchIdResult(SearchResponse response, Function<String, ID> converter, ZoneId timeZone) {
     this.facets = new Facets(response, timeZone);
     this.total = response.getHits().getTotalHits().value;
     this.uuids = convertToIds(response.getHits(), converter);
index 996ba9afd1d0a624d2b50e121f0aac3bfe53313b..7dfd29893593035c9b7d47db02d5dd67de3fb777 100644 (file)
@@ -19,9 +19,9 @@
  */
 package org.sonar.server.es;
 
+import java.time.ZoneId;
 import java.util.List;
 import java.util.Map;
-import java.util.TimeZone;
 import java.util.function.Function;
 import org.apache.commons.lang.builder.ReflectionToStringBuilder;
 import org.elasticsearch.action.search.SearchResponse;
@@ -32,7 +32,7 @@ public class SearchResult<DOC extends BaseDoc> {
   private final Facets facets;
   private final long total;
 
-  public SearchResult(SearchResponse response, Function<Map<String, Object>, DOC> converter, TimeZone timeZone) {
+  public SearchResult(SearchResponse response, Function<Map<String, Object>, DOC> converter, ZoneId timeZone) {
     this.facets = new Facets(response, timeZone);
     this.total = response.getHits().getTotalHits().value;
     this.docs = EsUtils.convertToDocs(response.getHits(), converter);
index 3d3ddec151d4362060992d48fb596da22f22cc61..4ce3719c36302c3f229de824330bea5024dac6c7 100644 (file)
@@ -67,6 +67,7 @@ public class SearchRequest {
   private List<String> sansTop25;
   private List<String> sonarsourceSecurity;
   private List<String> cwe;
+  private String timeZone;
 
   public SearchRequest() {
     // nothing to do here
@@ -469,4 +470,14 @@ public class SearchRequest {
     this.pullRequest = pullRequest;
     return this;
   }
+
+  @CheckForNull
+  public String getTimeZone() {
+    return timeZone;
+  }
+
+  public SearchRequest setTimeZone(@Nullable String timeZone) {
+    this.timeZone = timeZone;
+    return this;
+  }
 }
index ead195c2e162f0a67035c67945ae4f7f1f910c1e..c21619b9464c26dc5326729486c363db2d41d803 100644 (file)
@@ -172,7 +172,7 @@ public class RuleIndex {
     SearchRequest esSearch = EsClient.prepareSearch(TYPE_RULE)
       .source(sourceBuilder);
 
-    return new SearchIdResult<>(client.search(esSearch), input -> input, system2.getDefaultTimeZone());
+    return new SearchIdResult<>(client.search(esSearch), input -> input, system2.getDefaultTimeZone().toZoneId());
   }
 
   /**
index 75d142cd6eee27976a1261976b929c40ef089c9d..de6f7a0206ec9ba5b66e437f18869fa7a8380263 100644 (file)
@@ -115,7 +115,7 @@ public class UserIndex {
 
     SearchRequest request = EsClient.prepareSearch(UserIndexDefinition.TYPE_USER)
       .source(searchSourceBuilder.query(boolQuery().must(esQuery).filter(filter)));
-    return new SearchResult<>(esClient.search(request), UserDoc::new, system2.getDefaultTimeZone());
+    return new SearchResult<>(esClient.search(request), UserDoc::new, system2.getDefaultTimeZone().toZoneId());
   }
 
 }
index c964c62691183111279ee420504d1d0424439ab7..97f04d40be520415ef35f7640e6e617d2141d842 100644 (file)
@@ -64,7 +64,11 @@ const fetchIssues = (query: T.RawQuery, requestOrganizations = true) => (
   getState: () => Store
 ) => {
   const organizationsEnabled = areThereCustomOrganizations(getState());
-  return searchIssues({ ...query, additionalFields: '_all' })
+  return searchIssues({
+    ...query,
+    additionalFields: '_all',
+    timeZone: Intl.DateTimeFormat().resolvedOptions().timeZone
+  })
     .then(response => {
       const parsedIssues = response.issues.map(issue =>
         parseIssueFromResponse(issue, response.components, response.users, response.rules)
index 4425cb5ae79cf54774390ded2951d34bef99ff99..0227e4908f3d597e824cdd1b340cc2fcb8f98baa 100644 (file)
@@ -32,6 +32,7 @@ import org.junit.Before;
 import org.junit.Test;
 import org.junit.rules.ExpectedException;
 import org.junit.runner.RunWith;
+import org.sonar.api.impl.utils.TestSystem2;
 import org.sonar.api.resources.Language;
 import org.sonar.api.resources.Languages;
 import org.sonar.api.rule.RuleKey;
@@ -41,7 +42,6 @@ import org.sonar.api.rules.RuleType;
 import org.sonar.api.server.debt.DebtRemediationFunction;
 import org.sonar.api.server.rule.RulesDefinition;
 import org.sonar.api.utils.DateUtils;
-import org.sonar.api.utils.System2;
 import org.sonar.api.utils.log.LogTester;
 import org.sonar.core.util.UuidFactory;
 import org.sonar.core.util.UuidFactoryFast;
@@ -103,7 +103,7 @@ public class RegisterRulesTest {
   private static final RuleKey RULE_KEY3 = RuleKey.of("fake", "rule3");
   private static final RuleKey HOTSPOT_RULE_KEY = RuleKey.of("fake", "hotspot");
 
-  private System2 system = mock(System2.class);
+  private TestSystem2 system = new TestSystem2().setNow(DATE1.getTime());
 
   @org.junit.Rule
   public ExpectedException expectedException = ExpectedException.none();
@@ -125,7 +125,6 @@ public class RegisterRulesTest {
 
   @Before
   public void before() {
-    when(system.now()).thenReturn(DATE1.getTime());
     ruleIndexer = new RuleIndexer(es.client(), dbClient);
     ruleIndex = new RuleIndex(es.client(), system);
     activeRuleIndexer = new ActiveRuleIndexer(dbClient, es.client());
@@ -327,7 +326,7 @@ public class RegisterRulesTest {
     dbClient.ruleDao().insertOrUpdate(db.getSession(), rule1.getMetadata());
     db.getSession().commit();
 
-    when(system.now()).thenReturn(DATE2.getTime());
+    system.setNow(DATE2.getTime());
     execute(new FakeRepositoryV2());
 
     verifyIndicesNotMarkedAsInitialized();
@@ -429,7 +428,7 @@ public class RegisterRulesTest {
 
   @Test
   public void update_only_rule_name() {
-    when(system.now()).thenReturn(DATE1.getTime());
+    system.setNow(DATE1.getTime());
     execute(context -> {
       NewRepository repo = context.createRepository("fake", "java");
       repo.createRule("rule")
@@ -438,7 +437,7 @@ public class RegisterRulesTest {
       repo.done();
     });
 
-    when(system.now()).thenReturn(DATE2.getTime());
+    system.setNow(DATE2.getTime());
     execute(context -> {
       NewRepository repo = context.createRepository("fake", "java");
       repo.createRule("rule")
@@ -458,7 +457,7 @@ public class RegisterRulesTest {
 
   @Test
   public void update_template_rule_key_should_also_update_custom_rules() {
-    when(system.now()).thenReturn(DATE1.getTime());
+    system.setNow(DATE1.getTime());
     execute(context -> {
       NewRepository repo = context.createRepository("squid", "java");
       repo.createRule("rule")
@@ -501,7 +500,7 @@ public class RegisterRulesTest {
     String ruleKey2 = "rule2";
     String repository = "fake";
 
-    when(system.now()).thenReturn(DATE1.getTime());
+    system.setNow(DATE1.getTime());
     execute(context -> {
       NewRepository repo = context.createRepository(repository, "java");
       repo.createRule(ruleKey1)
@@ -515,7 +514,7 @@ public class RegisterRulesTest {
     assertThat(searchRule1.getUuids()).containsOnly(rule1.getUuid());
     assertThat(searchRule1.getTotal()).isEqualTo(1);
 
-    when(system.now()).thenReturn(DATE2.getTime());
+    system.setNow(DATE2.getTime());
     execute(context -> {
       NewRepository repo = context.createRepository(repository, "java");
       repo.createRule(ruleKey2)
@@ -543,7 +542,7 @@ public class RegisterRulesTest {
     String repository1 = "fake1";
     String repository2 = "fake2";
 
-    when(system.now()).thenReturn(DATE1.getTime());
+    system.setNow(DATE1.getTime());
     execute(context -> {
       NewRepository repo = context.createRepository(repository1, "java");
       repo.createRule(ruleKey)
@@ -557,7 +556,7 @@ public class RegisterRulesTest {
     assertThat(searchRule1.getUuids()).containsOnly(rule1.getUuid());
     assertThat(searchRule1.getTotal()).isEqualTo(1);
 
-    when(system.now()).thenReturn(DATE2.getTime());
+    system.setNow(DATE2.getTime());
     execute(context -> {
       NewRepository repo = context.createRepository(repository2, "java");
       repo.createRule(ruleKey)
@@ -584,7 +583,7 @@ public class RegisterRulesTest {
   public void update_if_only_renamed_and_deprecated_key_declared(String ruleKey1, String repo1, String ruleKey2, String repo2) {
     String name = "Name1";
     String description = "Description";
-    when(system.now()).thenReturn(DATE1.getTime());
+    system.setNow(DATE1.getTime());
     execute(context -> {
       NewRepository repo = context.createRepository(repo1, "java");
       repo.createRule(ruleKey1)
@@ -597,7 +596,7 @@ public class RegisterRulesTest {
     assertThat(ruleIndex.search(new RuleQuery().setQueryText(name), new SearchOptions()).getUuids())
       .containsOnly(rule1.getUuid());
 
-    when(system.now()).thenReturn(DATE2.getTime());
+    system.setNow(DATE2.getTime());
     execute(context -> {
       NewRepository repo = context.createRepository(repo2, "java");
       repo.createRule(ruleKey2)
@@ -633,7 +632,7 @@ public class RegisterRulesTest {
     String repository1 = "fake1";
     String repository2 = "fake2";
 
-    when(system.now()).thenReturn(DATE1.getTime());
+    system.setNow(DATE1.getTime());
     execute(context -> {
       NewRepository repo = context.createRepository(repository1, "java");
       repo.createRule(ruleKey1)
@@ -646,7 +645,7 @@ public class RegisterRulesTest {
     assertThat(ruleIndex.search(new RuleQuery().setQueryText("Name1"), new SearchOptions()).getUuids())
       .containsOnly(rule1.getUuid());
 
-    when(system.now()).thenReturn(DATE2.getTime());
+    system.setNow(DATE2.getTime());
     execute(context -> {
       NewRepository repo = context.createRepository(repository2, "java");
       repo.createRule(ruleKey2)
@@ -668,7 +667,7 @@ public class RegisterRulesTest {
 
   @Test
   public void update_only_rule_description() {
-    when(system.now()).thenReturn(DATE1.getTime());
+    system.setNow(DATE1.getTime());
     execute(context -> {
       NewRepository repo = context.createRepository("fake", "java");
       repo.createRule("rule")
@@ -677,7 +676,7 @@ public class RegisterRulesTest {
       repo.done();
     });
 
-    when(system.now()).thenReturn(DATE2.getTime());
+    system.setNow(DATE2.getTime());
     execute(context -> {
       NewRepository repo = context.createRepository("fake", "java");
       repo.createRule("rule")
@@ -698,7 +697,7 @@ public class RegisterRulesTest {
   @Test
   public void rule_previously_created_as_adhoc_becomes_none_adhoc() {
     RuleDefinitionDto rule = db.rules().insert(r -> r.setRepositoryKey("external_fake").setIsExternal(true).setIsAdHoc(true));
-    when(system.now()).thenReturn(DATE2.getTime());
+    system.setNow(DATE2.getTime());
     execute(context -> {
       NewRepository repo = context.createExternalRepository("fake", rule.getLanguage());
       repo.createRule(rule.getRuleKey())
@@ -740,11 +739,11 @@ public class RegisterRulesTest {
   @Test
   public void disable_then_enable_rule() {
     // Install rule
-    when(system.now()).thenReturn(DATE1.getTime());
+    system.setNow(DATE1.getTime());
     execute(new FakeRepositoryV1());
 
     // Uninstall rule
-    when(system.now()).thenReturn(DATE2.getTime());
+    system.setNow(DATE2.getTime());
     execute();
 
     RuleDto rule = dbClient.ruleDao().selectOrFailByKey(db.getSession(), RULE_KEY1);
@@ -752,7 +751,7 @@ public class RegisterRulesTest {
     assertThat(ruleIndex.search(new RuleQuery().setKey(RULE_KEY1.toString()), new SearchOptions()).getTotal()).isEqualTo(0);
 
     // Re-install rule
-    when(system.now()).thenReturn(DATE3.getTime());
+    system.setNow(DATE3.getTime());
     execute(new FakeRepositoryV1());
 
     rule = dbClient.ruleDao().selectOrFailByKey(db.getSession(), RULE_KEY1);
@@ -765,7 +764,7 @@ public class RegisterRulesTest {
     execute(new FakeRepositoryV1());
     assertThat(dbClient.ruleDao().selectAllDefinitions(db.getSession())).hasSize(3);
 
-    when(system.now()).thenReturn(DATE2.getTime());
+    system.setNow(DATE2.getTime());
     execute(new FakeRepositoryV1());
 
     RuleDto rule1 = dbClient.ruleDao().selectOrFailByKey(db.getSession(), RULE_KEY1);
@@ -785,7 +784,7 @@ public class RegisterRulesTest {
 
     assertThat(rule2.getStatus()).isEqualTo(READY);
 
-    when(system.now()).thenReturn(DATE2.getTime());
+    system.setNow(DATE2.getTime());
     execute(new FakeRepositoryV2());
 
     // On MySQL, need to update a rule otherwise rule2 will be seen as READY, but why ???
@@ -799,7 +798,7 @@ public class RegisterRulesTest {
 
     assertThat(ruleIndex.search(new RuleQuery(), new SearchOptions()).getUuids()).containsOnly(rule1.getUuid(), rule3.getUuid());
 
-    when(system.now()).thenReturn(DATE3.getTime());
+    system.setNow(DATE3.getTime());
     execute(new FakeRepositoryV2());
     db.getSession().commit();
 
index 489e18e758be798e9e7125a12a860fb3b5871c52..886f81583ec17771e61ef75e841aed05eb84aca4 100644 (file)
@@ -104,7 +104,7 @@ public class ComponentIndex {
 
     SearchRequest request = EsClient.prepareSearch(TYPE_COMPONENT.getMainType())
       .source(source);
-    return new SearchIdResult<>(client.search(request), id -> id, system2.getDefaultTimeZone());
+    return new SearchIdResult<>(client.search(request), id -> id, system2.getDefaultTimeZone().toZoneId());
   }
 
   public ComponentIndexResults searchSuggestions(SuggestionQuery query) {
index 1446e48f4ab41a1e6f43849a1c8bc2334053cee0..0f016ac6c263c1d8ff2ccfe2b796f616782ba1ed 100644 (file)
@@ -809,7 +809,7 @@ public class IssueIndex {
           .dateHistogramInterval(bucketSize)
           .minDocCount(0L)
           .format(DateUtils.DATETIME_FORMAT)
-          .timeZone(system.getDefaultTimeZone().toZoneId())
+          .timeZone(Optional.ofNullable(query.timeZone()).orElse(system.getDefaultTimeZone().toZoneId()))
           // ES dateHistogram bounds are inclusive while createdBefore parameter is exclusive
           .extendedBounds(new ExtendedBounds(startInclusive ? startTime : (startTime + 1), endTime - 1L));
         addEffortAggregationIfNeeded(query, dateHistogram);
index c58511323c8c44b547d819fca520d3292d2f359f..38eb36bb7afd4944dc27e23d9f533731bd8a24d5 100644 (file)
@@ -20,6 +20,7 @@
 package org.sonar.server.issue.index;
 
 import com.google.common.collect.ImmutableSet;
+import java.time.ZoneId;
 import java.util.Collection;
 import java.util.Collections;
 import java.util.Date;
@@ -96,6 +97,7 @@ public class IssueQuery {
   private final String organizationUuid;
   private final String branchUuid;
   private final boolean mainBranch;
+  private final ZoneId timeZone;
 
   private IssueQuery(Builder builder) {
     this.issueKeys = defaultCollection(builder.issueKeys);
@@ -133,6 +135,7 @@ public class IssueQuery {
     this.organizationUuid = builder.organizationUuid;
     this.branchUuid = builder.branchUuid;
     this.mainBranch = builder.mainBranch;
+    this.timeZone = builder.timeZone;
   }
 
   public Collection<String> issueKeys() {
@@ -294,6 +297,11 @@ public class IssueQuery {
     return new Builder();
   }
 
+  @CheckForNull
+  public ZoneId timeZone() {
+    return timeZone;
+  }
+
   public static class Builder {
     private Collection<String> issueKeys;
     private Collection<String> severities;
@@ -330,6 +338,7 @@ public class IssueQuery {
     private String organizationUuid;
     private String branchUuid;
     private boolean mainBranch = true;
+    private ZoneId timeZone;
 
     private Builder() {
 
@@ -536,6 +545,11 @@ public class IssueQuery {
       this.mainBranch = mainBranch;
       return this;
     }
+
+    public Builder timeZone(ZoneId timeZone) {
+      this.timeZone = timeZone;
+      return this;
+    }
   }
 
   private static <T> Collection<T> defaultCollection(@Nullable Collection<T> c) {
index 3e93bf5c4ef38c1acc0a8d40643127caa2c546fc..65b79e2c50937636b8a0bed06cf83668207c2917 100644 (file)
@@ -23,8 +23,10 @@ import com.google.common.base.Joiner;
 import com.google.common.base.Strings;
 import com.google.common.collect.ImmutableList;
 import java.time.Clock;
+import java.time.DateTimeException;
 import java.time.OffsetDateTime;
 import java.time.Period;
+import java.time.ZoneId;
 import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.Collection;
@@ -66,7 +68,6 @@ import static org.sonar.api.issue.Issue.STATUSES;
 import static org.sonar.api.issue.Issue.STATUS_REVIEWED;
 import static org.sonar.api.issue.Issue.STATUS_TO_REVIEW;
 import static org.sonar.api.utils.DateUtils.longToDate;
-import static org.sonar.api.utils.DateUtils.parseDateOrDateTime;
 import static org.sonar.api.utils.DateUtils.parseEndingDateOrDateTime;
 import static org.sonar.api.utils.DateUtils.parseStartingDateOrDateTime;
 import static org.sonar.core.util.stream.MoreCollectors.toHashSet;
@@ -108,6 +109,7 @@ public class IssueQueryFactory {
 
   public IssueQuery create(SearchRequest request) {
     try (DbSession dbSession = dbClient.openSession(false)) {
+      final ZoneId timeZone = parseTimeZone(request.getTimeZone()).orElse(clock.getZone());
       IssueQuery.Builder builder = IssueQuery.builder()
         .issueKeys(request.getIssues())
         .severities(request.getSeverities())
@@ -126,16 +128,17 @@ public class IssueQueryFactory {
         .cwe(request.getCwe())
         .sonarsourceSecurity(request.getSonarsourceSecurity())
         .assigned(request.getAssigned())
-        .createdAt(parseDateOrDateTime(request.getCreatedAt()))
-        .createdBefore(parseEndingDateOrDateTime(request.getCreatedBefore()))
+        .createdAt(parseStartingDateOrDateTime(request.getCreatedAt(), timeZone))
+        .createdBefore(parseEndingDateOrDateTime(request.getCreatedBefore(), timeZone))
         .facetMode(request.getFacetMode())
-        .organizationUuid(convertOrganizationKeyToUuid(dbSession, request.getOrganization()));
+        .organizationUuid(convertOrganizationKeyToUuid(dbSession, request.getOrganization()))
+        .timeZone(timeZone);
 
       List<ComponentDto> allComponents = new ArrayList<>();
       boolean effectiveOnComponentOnly = mergeDeprecatedComponentParameters(dbSession, request, allComponents);
       addComponentParameters(builder, dbSession, effectiveOnComponentOnly, allComponents, request);
 
-      setCreatedAfterFromRequest(dbSession, builder, request, allComponents);
+      setCreatedAfterFromRequest(dbSession, builder, request, allComponents, timeZone);
       String sort = request.getSort();
       if (!Strings.isNullOrEmpty(sort)) {
         builder.sort(sort);
@@ -145,6 +148,17 @@ public class IssueQueryFactory {
     }
   }
 
+  private Optional<ZoneId> parseTimeZone(@Nullable String timeZone) {
+    if (timeZone == null) {
+      return Optional.empty();
+    }
+    try {
+      return Optional.of(ZoneId.of(timeZone));
+    } catch (DateTimeException e) {
+      throw new IllegalArgumentException("TimeZone '" + timeZone + "' cannot be parsed as a valid zone ID");
+    }
+  }
+
   private void setCreatedAfterFromDates(IssueQuery.Builder builder, @Nullable Date createdAfter, @Nullable String createdInLast, boolean createdAfterInclusive) {
     Date actualCreatedAfter = createdAfter;
     if (createdInLast != null) {
@@ -165,8 +179,8 @@ public class IssueQueryFactory {
     return organization.map(OrganizationDto::getUuid).orElse(UNKNOWN);
   }
 
-  private void setCreatedAfterFromRequest(DbSession dbSession, IssueQuery.Builder builder, SearchRequest request, List<ComponentDto> componentUuids) {
-    Date createdAfter = parseStartingDateOrDateTime(request.getCreatedAfter());
+  private void setCreatedAfterFromRequest(DbSession dbSession, IssueQuery.Builder builder, SearchRequest request, List<ComponentDto> componentUuids, ZoneId timeZone) {
+    Date createdAfter = parseStartingDateOrDateTime(request.getCreatedAfter(), timeZone);
     String createdInLast = request.getCreatedInLast();
 
     if (request.getSinceLeakPeriod() == null || !request.getSinceLeakPeriod()) {
index 4231f0d42be7ec1568ae474f49f38889864a7550..bf81c3a02d747cd5404e0b66ed72c792a44039e0 100644 (file)
@@ -226,7 +226,7 @@ public class ProjectMeasuresIndex {
     filtersComputer.getPostFilters().ifPresent(searchSourceBuilder::postFilter);
     SearchResponse response = client.search(EsClient.prepareSearch(TYPE_PROJECT_MEASURES.getMainType())
       .source(searchSourceBuilder));
-    return new SearchIdResult<>(response, id -> id, system2.getDefaultTimeZone());
+    return new SearchIdResult<>(response, id -> id, system2.getDefaultTimeZone().toZoneId());
   }
 
   private static RequestFiltersComputer createFiltersComputer(SearchOptions searchOptions, AllFilters allFilters) {
index 74d67113b28f9171846e67e6dbb028c30d992fac..35ed6595a36010f37074b9ba9d4bcba5f93c473d 100644 (file)
@@ -197,7 +197,7 @@ public class IssueIndexDebtTest {
       IssueDocTesting.newDoc("I3", file).setAssigneeUuid("uuid-simon").setEffort(10L),
       IssueDocTesting.newDoc("I4", file).setAssigneeUuid(null).setEffort(10L));
 
-    Facets facets = new Facets(underTest.search(newQueryBuilder().build(), new SearchOptions().addFacets(asList("assignees"))), system2.getDefaultTimeZone());
+    Facets facets = new Facets(underTest.search(newQueryBuilder().build(), new SearchOptions().addFacets(asList("assignees"))), system2.getDefaultTimeZone().toZoneId());
     assertThat(facets.getNames()).containsOnly("assignees", FACET_MODE_EFFORT);
     assertThat(facets.get("assignees")).containsOnly(entry("uuid-steph", 10L), entry("uuid-simon", 20L), entry("", 10L));
     assertThat(facets.get(FACET_MODE_EFFORT)).containsOnly(entry("total", 40L));
@@ -214,7 +214,7 @@ public class IssueIndexDebtTest {
       IssueDocTesting.newDoc("I3", file).setAuthorLogin("simon").setEffort(10L),
       IssueDocTesting.newDoc("I4", file).setAuthorLogin(null).setEffort(10L));
 
-    Facets facets = new Facets(underTest.search(newQueryBuilder().build(), new SearchOptions().addFacets(asList("authors"))), system2.getDefaultTimeZone());
+    Facets facets = new Facets(underTest.search(newQueryBuilder().build(), new SearchOptions().addFacets(asList("authors"))), system2.getDefaultTimeZone().toZoneId());
     assertThat(facets.getNames()).containsOnly("authors", FACET_MODE_EFFORT);
     assertThat(facets.get("authors")).containsOnly(entry("steph", 10L), entry("simon", 20L));
     assertThat(facets.get(FACET_MODE_EFFORT)).containsOnly(entry("total", 40L));
@@ -225,7 +225,7 @@ public class IssueIndexDebtTest {
     SearchOptions searchOptions = fixtureForCreatedAtFacet();
 
     Builder query = newQueryBuilder().createdBefore(parseDateTime("2016-01-01T00:00:00+0100"));
-    Map<String, Long> createdAt = new Facets(underTest.search(query.build(), searchOptions), system2.getDefaultTimeZone()).get("createdAt");
+    Map<String, Long> createdAt = new Facets(underTest.search(query.build(), searchOptions), system2.getDefaultTimeZone().toZoneId()).get("createdAt");
     assertThat(createdAt).containsOnly(
       entry("2011-01-01", 10L),
       entry("2012-01-01", 0L),
@@ -257,7 +257,7 @@ public class IssueIndexDebtTest {
   }
 
   private Facets search(String additionalFacet) {
-    return new Facets(underTest.search(newQueryBuilder().build(), new SearchOptions().addFacets(singletonList(additionalFacet))), system2.getDefaultTimeZone());
+    return new Facets(underTest.search(newQueryBuilder().build(), new SearchOptions().addFacets(singletonList(additionalFacet))), system2.getDefaultTimeZone().toZoneId());
   }
 
   private Builder newQueryBuilder() {
index 11437d42c902b855cc19be4faa85590ff5876138..fb9bbaa04eb30c7e435b2c6cfbae9e3478387b96 100644 (file)
  */
 package org.sonar.server.issue.index;
 
+import java.time.ZoneId;
+import java.util.Date;
 import java.util.Map;
+import java.util.TimeZone;
 import org.elasticsearch.action.search.SearchResponse;
 import org.junit.Rule;
 import org.junit.Test;
@@ -80,7 +83,8 @@ public class IssueIndexFacetsTest {
   public UserSessionRule userSessionRule = UserSessionRule.standalone();
   @Rule
   public ExpectedException expectedException = none();
-  private System2 system2 = new TestSystem2().setNow(1_500_000_000_000L).setDefaultTimeZone(getTimeZone("GMT-01:00"));
+  private final TimeZone defaultTimezone = getTimeZone("GMT-01:00");
+  private System2 system2 = new TestSystem2().setNow(1_500_000_000_000L).setDefaultTimeZone(defaultTimezone);
   @Rule
   public DbTester db = DbTester.create(system2);
 
@@ -431,7 +435,7 @@ public class IssueIndexFacetsTest {
   }
 
   @Test
-  public void facet_on_created_at_with_less_than_20_days() {
+  public void facet_on_created_at_with_less_than_20_days_use_system_timezone_by_default() {
     SearchOptions options = fixtureForCreatedAtFacet();
 
     IssueQuery query = IssueQuery.builder()
@@ -439,7 +443,7 @@ public class IssueIndexFacetsTest {
       .createdBefore(parseDateTime("2014-09-08T00:00:00+0100"))
       .build();
     SearchResponse result = underTest.search(query, options);
-    Map<String, Long> buckets = new Facets(result, system2.getDefaultTimeZone()).get("createdAt");
+    Map<String, Long> buckets = new Facets(result, system2.getDefaultTimeZone().toZoneId()).get("createdAt");
     assertThat(buckets).containsOnly(
       entry("2014-08-31", 0L),
       entry("2014-09-01", 2L),
@@ -451,6 +455,53 @@ public class IssueIndexFacetsTest {
       entry("2014-09-07", 0L));
   }
 
+  @Test
+  public void facet_on_created_at_with_less_than_20_days_use_user_timezone_if_provided() {
+    // Use timezones very far from each other in order to see some issues moving to a different calendar day
+    final ZoneId plus14 = ZoneId.of("Pacific/Kiritimati");
+    final ZoneId minus11 = ZoneId.of("Pacific/Pago_Pago");
+
+
+    SearchOptions options = fixtureForCreatedAtFacet();
+
+    final Date startDate = parseDateTime("2014-09-01T00:00:00+0000");
+    final Date endDate = parseDateTime("2014-09-08T00:00:00+0000");
+
+    IssueQuery queryPlus14 = IssueQuery.builder()
+      .createdAfter(startDate)
+      .createdBefore(endDate)
+      .timeZone(plus14)
+      .build();
+    SearchResponse resultPlus14 = underTest.search(queryPlus14, options);
+    Map<String, Long> bucketsPlus14 = new Facets(resultPlus14, plus14).get("createdAt");
+    assertThat(bucketsPlus14).containsOnly(
+      entry("2014-09-01", 0L),
+      entry("2014-09-02", 2L),
+      entry("2014-09-03", 1L),
+      entry("2014-09-04", 0L),
+      entry("2014-09-05", 0L),
+      entry("2014-09-06", 1L),
+      entry("2014-09-07", 0L),
+      entry("2014-09-08", 0L));
+
+    IssueQuery queryMinus11 = IssueQuery.builder()
+      .createdAfter(startDate)
+      .createdBefore(endDate)
+      .timeZone(minus11)
+      .build();
+    SearchResponse resultMinus11 = underTest.search(queryMinus11, options);
+    Map<String, Long> bucketsMinus11 = new Facets(resultMinus11, minus11).get("createdAt");
+    assertThat(bucketsMinus11).containsOnly(
+      entry("2014-08-31", 1L),
+      entry("2014-09-01", 1L),
+      entry("2014-09-02", 1L),
+      entry("2014-09-03", 0L),
+      entry("2014-09-04", 0L),
+      entry("2014-09-05", 1L),
+      entry("2014-09-06", 0L),
+      entry("2014-09-07", 0L));
+  }
+
   @Test
   public void facet_on_created_at_with_less_than_20_weeks() {
     SearchOptions options = fixtureForCreatedAtFacet();
@@ -459,7 +510,7 @@ public class IssueIndexFacetsTest {
       .createdAfter(parseDateTime("2014-09-01T00:00:00+0100"))
       .createdBefore(parseDateTime("2014-09-21T00:00:00+0100")).build(),
       options);
-    Map<String, Long> createdAt = new Facets(result, system2.getDefaultTimeZone()).get("createdAt");
+    Map<String, Long> createdAt = new Facets(result, system2.getDefaultTimeZone().toZoneId()).get("createdAt");
     assertThat(createdAt).containsOnly(
       entry("2014-08-25", 0L),
       entry("2014-09-01", 4L),
@@ -475,7 +526,7 @@ public class IssueIndexFacetsTest {
       .createdAfter(parseDateTime("2014-09-01T00:00:00+0100"))
       .createdBefore(parseDateTime("2015-01-19T00:00:00+0100")).build(),
       options);
-    Map<String, Long> createdAt = new Facets(result, system2.getDefaultTimeZone()).get("createdAt");
+    Map<String, Long> createdAt = new Facets(result, system2.getDefaultTimeZone().toZoneId()).get("createdAt");
     assertThat(createdAt).containsOnly(
       entry("2014-08-01", 0L),
       entry("2014-09-01", 5L),
@@ -493,7 +544,7 @@ public class IssueIndexFacetsTest {
       .createdAfter(parseDateTime("2011-01-01T00:00:00+0100"))
       .createdBefore(parseDateTime("2016-01-01T00:00:00+0100")).build(),
       options);
-    Map<String, Long> createdAt = new Facets(result, system2.getDefaultTimeZone()).get("createdAt");
+    Map<String, Long> createdAt = new Facets(result, system2.getDefaultTimeZone().toZoneId()).get("createdAt");
     assertThat(createdAt).containsOnly(
       entry("2010-01-01", 0L),
       entry("2011-01-01", 1L),
@@ -511,7 +562,7 @@ public class IssueIndexFacetsTest {
       .createdAfter(parseDateTime("2014-09-01T00:00:00-0100"))
       .createdBefore(parseDateTime("2014-09-02T00:00:00-0100")).build(),
       options);
-    Map<String, Long> createdAt = new Facets(result, system2.getDefaultTimeZone()).get("createdAt");
+    Map<String, Long> createdAt = new Facets(result, system2.getDefaultTimeZone().toZoneId()).get("createdAt");
     assertThat(createdAt).containsOnly(
       entry("2014-09-01", 2L));
   }
@@ -524,7 +575,7 @@ public class IssueIndexFacetsTest {
       .createdAfter(parseDateTime("2009-01-01T00:00:00+0100"))
       .createdBefore(parseDateTime("2016-01-01T00:00:00+0100"))
       .build(), options);
-    Map<String, Long> createdAt = new Facets(result, system2.getDefaultTimeZone()).get("createdAt");
+    Map<String, Long> createdAt = new Facets(result, system2.getDefaultTimeZone().toZoneId()).get("createdAt");
     assertThat(createdAt).containsOnly(
       entry("2008-01-01", 0L),
       entry("2009-01-01", 0L),
@@ -543,7 +594,7 @@ public class IssueIndexFacetsTest {
     SearchResponse result = underTest.search(IssueQuery.builder()
       .createdBefore(parseDateTime("2016-01-01T00:00:00+0100")).build(),
       searchOptions);
-    Map<String, Long> createdAt = new Facets(result, system2.getDefaultTimeZone()).get("createdAt");
+    Map<String, Long> createdAt = new Facets(result, system2.getDefaultTimeZone().toZoneId()).get("createdAt");
     assertThat(createdAt).containsOnly(
       entry("2011-01-01", 1L),
       entry("2012-01-01", 0L),
@@ -557,7 +608,7 @@ public class IssueIndexFacetsTest {
     SearchOptions searchOptions = new SearchOptions().addFacets("createdAt");
 
     SearchResponse result = underTest.search(IssueQuery.builder().build(), searchOptions);
-    Map<String, Long> createdAt = new Facets(result, system2.getDefaultTimeZone()).get("createdAt");
+    Map<String, Long> createdAt = new Facets(result, system2.getDefaultTimeZone().toZoneId()).get("createdAt");
     assertThat(createdAt).isNull();
   }
 
@@ -566,12 +617,12 @@ public class IssueIndexFacetsTest {
     ComponentDto file = newFileDto(project, null);
 
     IssueDoc issue0 = newDoc("ISSUE0", file).setFuncCreationDate(parseDateTime("2011-04-25T00:05:13+0000"));
-    IssueDoc issue1 = newDoc("I1", file).setFuncCreationDate(parseDateTime("2014-09-01T12:34:56+0100"));
-    IssueDoc issue2 = newDoc("I2", file).setFuncCreationDate(parseDateTime("2014-09-01T10:46:00-1200"));
-    IssueDoc issue3 = newDoc("I3", file).setFuncCreationDate(parseDateTime("2014-09-02T23:34:56+1200"));
-    IssueDoc issue4 = newDoc("I4", file).setFuncCreationDate(parseDateTime("2014-09-05T12:34:56+0100"));
-    IssueDoc issue5 = newDoc("I5", file).setFuncCreationDate(parseDateTime("2014-09-20T12:34:56+0100"));
-    IssueDoc issue6 = newDoc("I6", file).setFuncCreationDate(parseDateTime("2015-01-18T12:34:56+0100"));
+    IssueDoc issue1 = newDoc("I1", file).setFuncCreationDate(parseDateTime("2014-09-01T10:34:56+0000"));
+    IssueDoc issue2 = newDoc("I2", file).setFuncCreationDate(parseDateTime("2014-09-01T22:46:00+0000"));
+    IssueDoc issue3 = newDoc("I3", file).setFuncCreationDate(parseDateTime("2014-09-02T11:34:56+0000"));
+    IssueDoc issue4 = newDoc("I4", file).setFuncCreationDate(parseDateTime("2014-09-05T11:34:56+0000"));
+    IssueDoc issue5 = newDoc("I5", file).setFuncCreationDate(parseDateTime("2014-09-20T11:34:56+0000"));
+    IssueDoc issue6 = newDoc("I6", file).setFuncCreationDate(parseDateTime("2015-01-18T11:34:56+0000"));
 
     indexIssues(issue0, issue1, issue2, issue3, issue4, issue5, issue6);
 
@@ -586,7 +637,7 @@ public class IssueIndexFacetsTest {
   @SafeVarargs
   private final void assertThatFacetHasExactly(IssueQuery.Builder query, String facet, Map.Entry<String, Long>... expectedEntries) {
     SearchResponse result = underTest.search(query.build(), new SearchOptions().addFacets(singletonList(facet)));
-    Facets facets = new Facets(result, system2.getDefaultTimeZone());
+    Facets facets = new Facets(result, system2.getDefaultTimeZone().toZoneId());
     assertThat(facets.getNames()).containsOnly(facet, "effort");
     assertThat(facets.get(facet)).containsExactly(expectedEntries);
   }
@@ -594,14 +645,14 @@ public class IssueIndexFacetsTest {
   @SafeVarargs
   private final void assertThatFacetHasOnly(IssueQuery.Builder query, String facet, Map.Entry<String, Long>... expectedEntries) {
     SearchResponse result = underTest.search(query.build(), new SearchOptions().addFacets(singletonList(facet)));
-    Facets facets = new Facets(result, system2.getDefaultTimeZone());
+    Facets facets = new Facets(result, system2.getDefaultTimeZone().toZoneId());
     assertThat(facets.getNames()).containsOnly(facet, "effort");
     assertThat(facets.get(facet)).containsOnly(expectedEntries);
   }
 
   private void assertThatFacetHasSize(IssueQuery issueQuery, String facet, int expectedSize) {
     SearchResponse result = underTest.search(issueQuery, new SearchOptions().addFacets(singletonList(facet)));
-    Facets facets = new Facets(result, system2.getDefaultTimeZone());
+    Facets facets = new Facets(result, system2.getDefaultTimeZone().toZoneId());
     assertThat(facets.get(facet)).hasSize(expectedSize);
   }
 }
index 054986e98fe5418670913d8a1c1db579aa29800f..cb5e38dcd29b2edb7641e07da3f61991727d01d2 100644 (file)
@@ -142,7 +142,7 @@ public class IssueIndexSecurityHotspotsTest {
   @SafeVarargs
   private final void assertThatFacetHasOnly(IssueQuery.Builder query, String facet, Map.Entry<String, Long>... expectedEntries) {
     SearchResponse result = underTest.search(query.build(), new SearchOptions().addFacets(singletonList(facet)));
-    Facets facets = new Facets(result, system2.getDefaultTimeZone());
+    Facets facets = new Facets(result, system2.getDefaultTimeZone().toZoneId());
     assertThat(facets.getNames()).containsOnly(facet, "effort");
     assertThat(facets.get(facet)).containsOnly(expectedEntries);
   }
index 0ebb3805a41539c4eef0e64ed948bef72292c183..d73c46b97ef03b2f712896a4b381ba5bc9361371 100644 (file)
@@ -20,6 +20,7 @@
 package org.sonar.server.issue.index;
 
 import java.time.Clock;
+import java.time.ZoneId;
 import java.time.ZoneOffset;
 import java.util.ArrayList;
 import java.util.Collections;
@@ -30,7 +31,6 @@ import org.junit.Test;
 import org.junit.rules.ExpectedException;
 import org.sonar.api.resources.Qualifiers;
 import org.sonar.api.rule.RuleKey;
-import org.sonar.api.utils.DateUtils;
 import org.sonar.db.DbTester;
 import org.sonar.db.component.ComponentDto;
 import org.sonar.db.component.SnapshotDto;
@@ -50,6 +50,7 @@ import static org.mockito.Mockito.mock;
 import static org.mockito.Mockito.when;
 import static org.sonar.api.resources.Qualifiers.APP;
 import static org.sonar.api.utils.DateUtils.addDays;
+import static org.sonar.api.utils.DateUtils.parseDateTime;
 import static org.sonar.api.web.UserRole.USER;
 import static org.sonar.db.component.ComponentTesting.newDirectory;
 import static org.sonar.db.component.ComponentTesting.newFileDto;
@@ -123,9 +124,9 @@ public class IssueQueryFactoryTest {
     assertThat(query.assigned()).isTrue();
     assertThat(query.rules()).hasSize(2);
     assertThat(query.directories()).containsOnly("aDirPath");
-    assertThat(query.createdAfter().date()).isEqualTo(DateUtils.parseDateTime("2013-04-16T09:08:24+0200"));
+    assertThat(query.createdAfter().date()).isEqualTo(parseDateTime("2013-04-16T09:08:24+0200"));
     assertThat(query.createdAfter().inclusive()).isTrue();
-    assertThat(query.createdBefore()).isEqualTo(DateUtils.parseDateTime("2013-04-17T09:08:24+0200"));
+    assertThat(query.createdBefore()).isEqualTo(parseDateTime("2013-04-17T09:08:24+0200"));
     assertThat(query.sort()).isEqualTo(IssueQuery.SORT_BY_CREATION_DATE);
     assertThat(query.asc()).isTrue();
   }
@@ -154,25 +155,49 @@ public class IssueQueryFactoryTest {
 
   @Test
   public void dates_are_inclusive() {
+    when(clock.getZone()).thenReturn(ZoneId.of("Europe/Paris"));
     SearchRequest request = new SearchRequest()
       .setCreatedAfter("2013-04-16")
       .setCreatedBefore("2013-04-17");
 
     IssueQuery query = underTest.create(request);
 
-    assertThat(query.createdAfter().date()).isEqualTo(DateUtils.parseDate("2013-04-16"));
+    assertThat(query.createdAfter().date()).isEqualTo(parseDateTime("2013-04-16T00:00:00+0200"));
     assertThat(query.createdAfter().inclusive()).isTrue();
-    assertThat(query.createdBefore()).isEqualTo(DateUtils.parseDate("2013-04-18"));
+    assertThat(query.createdBefore()).isEqualTo(parseDateTime("2013-04-18T00:00:00+0200"));
   }
 
   @Test
   public void creation_date_support_localdate() {
+    when(clock.getZone()).thenReturn(ZoneId.of("Europe/Paris"));
     SearchRequest request = new SearchRequest()
       .setCreatedAt("2013-04-16");
 
     IssueQuery query = underTest.create(request);
 
-    assertThat(query.createdAt()).isEqualTo(DateUtils.parseDate("2013-04-16"));
+    assertThat(query.createdAt()).isEqualTo(parseDateTime("2013-04-16T00:00:00+0200"));
+  }
+
+  @Test
+  public void use_provided_timezone_to_parse_createdAfter() {
+    SearchRequest request = new SearchRequest()
+      .setCreatedAfter("2020-04-16")
+      .setTimeZone("Europe/Volgograd");
+
+    IssueQuery query = underTest.create(request);
+
+    assertThat(query.createdAfter().date()).isEqualTo(parseDateTime("2020-04-16T00:00:00+0400"));
+  }
+
+  @Test
+  public void use_provided_timezone_to_parse_createdBefore() {
+    SearchRequest request = new SearchRequest()
+      .setCreatedBefore("2020-04-16")
+      .setTimeZone("Europe/Moscow");
+
+    IssueQuery query = underTest.create(request);
+
+    assertThat(query.createdBefore()).isEqualTo(parseDateTime("2020-04-17T00:00:00+0300"));
   }
 
   @Test
@@ -182,7 +207,7 @@ public class IssueQueryFactoryTest {
 
     IssueQuery query = underTest.create(request);
 
-    assertThat(query.createdAt()).isEqualTo(DateUtils.parseDateTime("2013-04-16T09:08:24+0200"));
+    assertThat(query.createdAt()).isEqualTo(parseDateTime("2013-04-16T09:08:24+0200"));
   }
 
   @Test
@@ -224,6 +249,17 @@ public class IssueQueryFactoryTest {
     underTest.create(request);
   }
 
+  @Test
+  public void fail_if_invalid_timezone() {
+    SearchRequest request = new SearchRequest()
+      .setTimeZone("Poitou-Charentes");
+
+    expectedException.expect(IllegalArgumentException.class);
+    expectedException.expectMessage("TimeZone 'Poitou-Charentes' cannot be parsed as a valid zone ID");
+
+    underTest.create(request);
+  }
+
   @Test
   public void param_componentUuids_enables_search_in_view_tree_if_user_has_permission_on_view() {
     ComponentDto view = db.components().insertView();
@@ -499,12 +535,12 @@ public class IssueQueryFactoryTest {
 
   @Test
   public void set_created_after_from_created_since() {
-    Date now = DateUtils.parseDateTime("2013-07-25T07:35:00+0100");
+    Date now = parseDateTime("2013-07-25T07:35:00+0100");
     when(clock.instant()).thenReturn(now.toInstant());
     when(clock.getZone()).thenReturn(ZoneOffset.UTC);
     SearchRequest request = new SearchRequest()
       .setCreatedInLast("1y2m3w4d");
-    assertThat(underTest.create(request).createdAfter().date()).isEqualTo(DateUtils.parseDateTime("2012-04-30T07:35:00+0100"));
+    assertThat(underTest.create(request).createdAfter().date()).isEqualTo(parseDateTime("2012-04-30T07:35:00+0100"));
     assertThat(underTest.create(request).createdAfter().inclusive()).isTrue();
 
   }
index 2ea5eb36b186df68e980d9442813908ab82d00c2..6ef83205c234dac16965bb607daeba0baeea0733 100644 (file)
@@ -26,6 +26,7 @@ import java.util.Arrays;
 import java.util.EnumSet;
 import java.util.List;
 import java.util.Map;
+import java.util.Optional;
 import java.util.Set;
 import java.util.stream.Collectors;
 import javax.annotation.Nullable;
@@ -128,6 +129,7 @@ import static org.sonarqube.ws.client.issue.IssuesWsParameters.PARAM_SINCE_LEAK_
 import static org.sonarqube.ws.client.issue.IssuesWsParameters.PARAM_SONARSOURCE_SECURITY;
 import static org.sonarqube.ws.client.issue.IssuesWsParameters.PARAM_STATUSES;
 import static org.sonarqube.ws.client.issue.IssuesWsParameters.PARAM_TAGS;
+import static org.sonarqube.ws.client.issue.IssuesWsParameters.PARAM_TIMEZONE;
 import static org.sonarqube.ws.client.issue.IssuesWsParameters.PARAM_TYPES;
 
 public class SearchAction implements IssuesWsAction {
@@ -172,7 +174,7 @@ public class SearchAction implements IssuesWsAction {
   private final DbClient dbClient;
 
   public SearchAction(UserSession userSession, IssueIndex issueIndex, IssueQueryFactory issueQueryFactory, IssueIndexSyncProgressChecker issueIndexSyncProgressChecker,
-    SearchResponseLoader searchResponseLoader, SearchResponseFormat searchResponseFormat, System2 system2, DbClient dbClient) {
+                      SearchResponseLoader searchResponseLoader, SearchResponseFormat searchResponseFormat, System2 system2, DbClient dbClient) {
     this.userSession = userSession;
     this.issueIndex = issueIndex;
     this.issueQueryFactory = issueQueryFactory;
@@ -192,8 +194,9 @@ public class SearchAction implements IssuesWsAction {
         + "<br/>When issue indexation is in progress returns 503 service unavailable HTTP code.")
       .setSince("3.6")
       .setChangelog(
+        new Change("8.6", "Parameter 'timeZone' added"),
         new Change("8.5", "Facet 'fileUuids' is dropped in favour of the new facet 'files'" +
-            "Note that they are not strictly identical, the latter returns the file paths."),
+          "Note that they are not strictly identical, the latter returns the file paths."),
         new Change("8.5", "Internal parameter 'fileUuids' has been dropped"),
         new Change("8.4", "parameters 'componentUuids', 'projectKeys' has been dropped."),
         new Change("8.2", "'REVIEWED', 'TO_REVIEW' status param values are no longer supported"),
@@ -303,12 +306,12 @@ public class SearchAction implements IssuesWsAction {
       .setExampleValue("2017-10-19T13:00:00+0200");
     action.createParam(PARAM_CREATED_AFTER)
       .setDescription("To retrieve issues created after the given date (inclusive). <br>" +
-        "Either a date (server timezone) or datetime can be provided. <br>" +
+        "Either a date (use '" + PARAM_TIMEZONE + "' attribute or it will default to server timezone) or datetime can be provided. <br>" +
         "If this parameter is set, createdSince must not be set")
       .setExampleValue("2017-10-19 or 2017-10-19T13:00:00+0200");
     action.createParam(PARAM_CREATED_BEFORE)
       .setDescription("To retrieve issues created before the given date (exclusive). <br>" +
-        "Either a date (server timezone) or datetime can be provided.")
+        "Either a date (use '" + PARAM_TIMEZONE + "' attribute or it will default to server timezone) or datetime can be provided.")
       .setExampleValue("2017-10-19 or 2017-10-19T13:00:00+0200");
     action.createParam(PARAM_CREATED_IN_LAST)
       .setDescription("To retrieve issues created during a time span before the current time (exclusive). " +
@@ -320,6 +323,11 @@ public class SearchAction implements IssuesWsAction {
         "If this parameter is set to a truthy value, createdAfter must not be set and one component uuid or key must be provided.")
       .setBooleanPossibleValues()
       .setDefaultValue("false");
+    action.createParam(PARAM_TIMEZONE)
+      .setDescription("To resolve dates passed to '" + PARAM_CREATED_AFTER + "' or '" + PARAM_CREATED_BEFORE + "' (does not apply to datetime) and to compute creation date histogram")
+      .setRequired(false)
+      .setExampleValue("'Europe/Paris', 'Z' or '+02:00'")
+      .setSince("8.6");
   }
 
   private static void addComponentRelatedParams(WebService.NewAction action) {
@@ -400,7 +408,7 @@ public class SearchAction implements IssuesWsAction {
       .filter(FACETS_REQUIRING_PROJECT_OR_ORGANIZATION::contains)
       .collect(toSet());
     checkArgument(facetsRequiringProjectOrOrganizationParameter.isEmpty() ||
-      (!query.projectUuids().isEmpty()) || query.organizationUuid() != null, "Facet(s) '%s' require to also filter by project or organization",
+        (!query.projectUuids().isEmpty()) || query.organizationUuid() != null, "Facet(s) '%s' require to also filter by project or organization",
       String.join(",", facetsRequiringProjectOrOrganizationParameter));
 
     // execute request
@@ -413,7 +421,7 @@ public class SearchAction implements IssuesWsAction {
     SearchResponseLoader.Collector collector = new SearchResponseLoader.Collector(issueKeys);
     collectLoggedInUser(collector);
     collectRequestParams(collector, request);
-    Facets facets = new Facets(result, system2.getDefaultTimeZone());
+    Facets facets = new Facets(result, Optional.ofNullable(query.timeZone()).orElse(system2.getDefaultTimeZone().toZoneId()));
     if (!options.getFacets().isEmpty()) {
       // add missing values to facets. For example if assignee "john" and facet on "assignees" are requested, then
       // "john" should always be listed in the facet. If it is not present, then it is added with value zero.
@@ -549,7 +557,8 @@ public class SearchAction implements IssuesWsAction {
       .setOwaspTop10(request.paramAsStrings(PARAM_OWASP_TOP_10))
       .setSansTop25(request.paramAsStrings(PARAM_SANS_TOP_25))
       .setCwe(request.paramAsStrings(PARAM_CWE))
-      .setSonarsourceSecurity(request.paramAsStrings(PARAM_SONARSOURCE_SECURITY));
+      .setSonarsourceSecurity(request.paramAsStrings(PARAM_SONARSOURCE_SECURITY))
+      .setTimeZone(request.param(PARAM_TIMEZONE));
   }
 
   private void checkIfNeedIssueSync(DbSession dbSession, SearchRequest searchRequest) {
index 8ce84c15a76c8d1b0387b4e433d6d19df89f368c..8dc98c832a014786474c0cca43e7d6a99539b342 100644 (file)
@@ -1332,7 +1332,7 @@ public class SearchActionTest {
       "createdAfter", "createdAt", "createdBefore", "createdInLast", "directories", "facetMode", "facets", "files", "issues", "scopes", "languages", "moduleUuids",
       "onComponentOnly",
       "p", "projects", "ps", "resolutions", "resolved", "rules", "s", "severities", "sinceLeakPeriod",
-      "statuses", "tags", "types", "owaspTop10", "sansTop25", "cwe", "sonarsourceSecurity");
+      "statuses", "tags", "types", "owaspTop10", "sansTop25", "cwe", "sonarsourceSecurity", "timeZone");
 
     assertThat(def.param("organization"))
       .matches(WebService.Param::isInternal)
index 569bb2b7abc72af6c8b20d39c758856213bac17c..77faa4457b45ab41dadbcdc86459c5d8df66537f 100644 (file)
@@ -21,6 +21,7 @@ package org.sonar.server.rule;
 
 import com.google.common.collect.ImmutableMap;
 import com.google.common.collect.Sets;
+import java.time.Instant;
 import java.util.Arrays;
 import java.util.Collections;
 import java.util.Date;
@@ -29,6 +30,7 @@ import org.assertj.core.api.Fail;
 import org.junit.Rule;
 import org.junit.Test;
 import org.junit.rules.ExpectedException;
+import org.sonar.api.impl.utils.TestSystem2;
 import org.sonar.api.rule.RuleKey;
 import org.sonar.api.rule.RuleStatus;
 import org.sonar.api.rule.Severity;
@@ -46,20 +48,18 @@ import org.sonar.db.rule.RuleTesting;
 import org.sonar.server.es.EsTester;
 import org.sonar.server.es.SearchOptions;
 import org.sonar.server.exceptions.BadRequestException;
-import org.sonar.server.organization.TestDefaultOrganizationProvider;
 import org.sonar.server.rule.index.RuleIndex;
 import org.sonar.server.rule.index.RuleIndexer;
 import org.sonar.server.rule.index.RuleQuery;
 
 import static org.assertj.core.api.Assertions.assertThat;
 import static org.junit.Assert.fail;
-import static org.mockito.Mockito.mock;
 import static org.sonar.db.rule.RuleTesting.newRule;
 import static org.sonar.server.util.TypeValidationsTesting.newFullTypeValidations;
 
 public class RuleCreatorTest {
 
-  private System2 system2 = mock(System2.class);
+  private System2 system2 = new TestSystem2().setNow(Instant.now().toEpochMilli());
 
   @Rule
   public ExpectedException expectedException = ExpectedException.none();
index 8450a557ab85eff3b665346b4645aa880583dfcf..5b7981176c606c9b1246acd8c97e8787d03b458e 100644 (file)
@@ -22,12 +22,14 @@ package org.sonar.server.rule;
 import com.google.common.collect.ImmutableMap;
 import com.google.common.collect.ImmutableSet;
 import com.google.common.collect.Sets;
+import java.time.Instant;
 import java.util.List;
 import java.util.Map;
 import java.util.stream.Collectors;
 import org.junit.Rule;
 import org.junit.Test;
 import org.junit.rules.ExpectedException;
+import org.sonar.api.impl.utils.TestSystem2;
 import org.sonar.api.rule.RuleKey;
 import org.sonar.api.rule.RuleStatus;
 import org.sonar.api.rule.Severity;
@@ -54,7 +56,6 @@ import org.sonar.server.rule.index.RuleQuery;
 import org.sonar.server.tester.UserSessionRule;
 
 import static org.assertj.core.api.Assertions.assertThat;
-import static org.mockito.Mockito.mock;
 import static org.sonar.api.rule.Severity.CRITICAL;
 import static org.sonar.db.rule.RuleTesting.newRule;
 import static org.sonar.server.rule.RuleUpdate.createForCustomRule;
@@ -64,7 +65,7 @@ public class RuleUpdaterTest {
 
   static final RuleKey RULE_KEY = RuleKey.of("squid", "S001");
 
-  private System2 system2 = mock(System2.class);
+  private System2 system2 = new TestSystem2().setNow(Instant.now().toEpochMilli());
 
   @Rule
   public ExpectedException expectedException = ExpectedException.none();
index cc5689a410387f419fa8d3fef74e7954d7c8b05b..47f2337c714691f33d1fdc6df970d8feba261fea 100644 (file)
@@ -25,7 +25,7 @@ import org.sonar.api.utils.System2;
 public class TestSystem2 extends System2 {
 
   private long now = 0L;
-  private TimeZone defaultTimeZone = getDefaultTimeZone();
+  private TimeZone defaultTimeZone = TimeZone.getTimeZone("UTC");
 
   public TestSystem2 setNow(long l) {
     this.now = l;
index 2cd20dbb1711d01c723afbf24c16a13e7e8400b4..4e2704574c5bd1ec62fda46c9176125afb5fa700 100644 (file)
@@ -227,14 +227,25 @@ public final class DateUtils {
   }
 
   /**
-   * Warning: may rely on default timezone!
+   * Warning: rely on default timezone!
    *
-   * @return the datetime, {@code null} if stringDate is null
-   * @throws IllegalArgumentException if stringDate is not a correctly formed date or datetime
+   * @see #parseDateOrDateTime(String, ZoneId) 
    * @since 6.1
    */
   @CheckForNull
   public static Date parseDateOrDateTime(@Nullable String stringDate) {
+    return parseDateOrDateTime(stringDate, ZoneId.systemDefault());
+  }
+
+  /**
+   * Parse either a full date time (using RFC-822 TZ format), or a local date.
+   * For local dates, the returned {@link Date} will be set at the beginning of the day, in the provided timezone.
+   * @return the datetime, {@code null} if stringDate is null
+   * @throws IllegalArgumentException if stringDate is not a correctly formed date or datetime
+   * @since 8.6
+   */
+  @CheckForNull
+  public static Date parseDateOrDateTime(@Nullable String stringDate, ZoneId timeZone) {
     if (stringDate == null) {
       return null;
     }
@@ -247,11 +258,11 @@ public final class DateUtils {
     LocalDate ld = parseLocalDateQuietly(stringDate);
     checkArgument(ld != null, "Date '%s' cannot be parsed as either a date or date+time", stringDate);
 
-    return Date.from(ld.atStartOfDay(ZoneId.systemDefault()).toInstant());
+    return Date.from(ld.atStartOfDay(timeZone).toInstant());
   }
 
   /**
-   * Warning: may rely on default timezone!
+   * Warning: rely on default timezone for local dates!
    *
    * @see #parseDateOrDateTime(String)
    */
@@ -261,29 +272,46 @@ public final class DateUtils {
   }
 
   /**
-   * Return the datetime if @param stringDate is a datetime, date + 1 day if stringDate is a date.
-   * So '2016-09-01' would return a date equivalent to '2016-09-02T00:00:00+0000' in GMT (Warning: relies on default timezone!)
+   * @see #parseDateOrDateTime(String, ZoneId)
+   */
+  @CheckForNull
+  public static Date parseStartingDateOrDateTime(@Nullable String stringDate, ZoneId timeZone) {
+    return parseDateOrDateTime(stringDate, timeZone);
+  }
+
+  /**
+   * Warning: rely on default timezone for local dates!
    *
-   * @return the datetime, {@code null} if stringDate is null
-   * @throws IllegalArgumentException if stringDate is not a correctly formed date or datetime
-   * @see #parseDateOrDateTime(String)
+   * @see #parseEndingDateOrDateTime(String, ZoneId)
    * @since 6.1
    */
   @CheckForNull
   public static Date parseEndingDateOrDateTime(@Nullable String stringDate) {
+    return parseEndingDateOrDateTime(stringDate, ZoneId.systemDefault());
+  }
+  /**
+   * Return the datetime if @param stringDate is a datetime, local date + 1 day if stringDate is a local date.
+   * So '2016-09-01' would return a date equivalent to '2016-09-02T00:00:00' in the provided timezone
+   *
+   * @return the datetime, {@code null} if stringDate is null
+   * @throws IllegalArgumentException if stringDate is not a correctly formed date or datetime
+   * @since 8.6
+   */
+  @CheckForNull
+  public static Date parseEndingDateOrDateTime(@Nullable String stringDate, ZoneId timeZone) {
     if (stringDate == null) {
       return null;
     }
 
-    Date date = parseDateTimeQuietly(stringDate);
-    if (date != null) {
-      return date;
+    OffsetDateTime odt = parseOffsetDateTimeQuietly(stringDate);
+    if (odt != null) {
+      return Date.from(odt.toInstant());
     }
 
-    date = parseDateQuietly(stringDate);
-    checkArgument(date != null, "Date '%s' cannot be parsed as either a date or date+time", stringDate);
+    LocalDate ld = parseLocalDateQuietly(stringDate);
+    checkArgument(ld != null, "Date '%s' cannot be parsed as either a date or date+time", stringDate);
 
-    return addDays(date, 1);
+    return Date.from(ld.atStartOfDay(timeZone).plusDays(1).toInstant());
   }
 
   /**
index 04283d2ec30410421e0f625ee7b6fb72d28feb4c..1281b641b076b7fd469b151102cb66aece33c5cb 100644 (file)
@@ -22,6 +22,9 @@ package org.sonar.api.utils;
 import com.tngtech.java.junit.dataprovider.DataProvider;
 import com.tngtech.java.junit.dataprovider.DataProviderRunner;
 import com.tngtech.java.junit.dataprovider.UseDataProvider;
+import java.time.LocalDate;
+import java.time.OffsetDateTime;
+import java.time.ZoneId;
 import java.util.Date;
 import org.junit.Rule;
 import org.junit.Test;
@@ -29,9 +32,7 @@ import org.junit.rules.ExpectedException;
 import org.junit.runner.RunWith;
 
 import static org.assertj.core.api.Assertions.assertThat;
-import static org.sonar.api.utils.DateUtils.parseDate;
 import static org.sonar.api.utils.DateUtils.parseDateOrDateTime;
-import static org.sonar.api.utils.DateUtils.parseDateTime;
 import static org.sonar.api.utils.DateUtils.parseEndingDateOrDateTime;
 import static org.sonar.api.utils.DateUtils.parseStartingDateOrDateTime;
 
@@ -139,8 +140,8 @@ public class DateUtilsTest {
   @DataProvider
   public static Object[][] date_times() {
     return new Object[][] {
-      {"2014-05-27", parseDate("2014-05-27")},
-      {"2014-05-27T15:50:45+0100", parseDateTime("2014-05-27T15:50:45+0100")},
+      {"2014-05-27", Date.from(LocalDate.parse("2014-05-27").atStartOfDay(ZoneId.systemDefault()).toInstant())},
+      {"2014-05-27T15:50:45+0100", Date.from(OffsetDateTime.parse("2014-05-27T15:50:45+01:00").toInstant())},
       {null, null}
     };
   }
@@ -152,19 +153,26 @@ public class DateUtilsTest {
     assertThat(parseStartingDateOrDateTime(stringDate)).isEqualTo(expectedDate);
   }
 
-  @DataProvider
-  public static Object[][] ending_date_times() {
-    return new Object[][] {
-      {"2014-05-27", parseDate("2014-05-28")},
-      {"2014-05-27T15:50:45+0100", parseDateTime("2014-05-27T15:50:45+0100")},
-      {null, null}
-    };
+  @Test
+  public void param_as__date_time_provided_timezone() {
+    final ZoneId zoneId = ZoneId.of("Europe/Moscow");
+    assertThat(parseDateOrDateTime("2020-05-27", zoneId)).isEqualTo(Date.from(OffsetDateTime.parse("2020-05-27T00:00:00+03:00").toInstant()));
+    assertThat(parseStartingDateOrDateTime("2020-05-27", zoneId)).isEqualTo(Date.from(OffsetDateTime.parse("2020-05-27T00:00:00+03:00").toInstant()));
+  }
+
+  @Test
+  public void param_as_ending_date_time_default_timezone() {
+    assertThat(parseEndingDateOrDateTime("2014-05-27")).isEqualTo(Date.from(LocalDate.parse("2014-05-28").atStartOfDay(ZoneId.systemDefault()).toInstant()));
+    assertThat(parseEndingDateOrDateTime("2014-05-27T15:50:45+0100")).isEqualTo(Date.from(OffsetDateTime.parse("2014-05-27T15:50:45+01:00").toInstant()));
+    assertThat(parseEndingDateOrDateTime(null)).isNull();
   }
 
   @Test
-  @UseDataProvider("ending_date_times")
-  public void param_as_ending_date_time(String stringDate, Date expectedDate) {
-    assertThat(parseEndingDateOrDateTime(stringDate)).isEqualTo(expectedDate);
+  public void param_as_ending_date_time_provided_timezone() {
+    final ZoneId zoneId = ZoneId.of("Europe/Moscow");
+    assertThat(parseEndingDateOrDateTime("2020-05-27", zoneId)).isEqualTo(Date.from(OffsetDateTime.parse("2020-05-28T00:00:00+03:00").toInstant()));
+    assertThat(parseEndingDateOrDateTime("2014-05-27T15:50:45+0100", zoneId)).isEqualTo(Date.from(OffsetDateTime.parse("2014-05-27T15:50:45+01:00").toInstant()));
+    assertThat(parseEndingDateOrDateTime(null, zoneId)).isNull();
   }
 
   @Test
index 2e81f8c3efa98bd6dc1bc13acf5ee0253fd0bae2..bca1096d0bd5cf213cc974c80e549f17fbde50b5 100644 (file)
@@ -99,6 +99,7 @@ public class IssuesWsParameters {
   public static final String PARAM_PAGE_INDEX = "pageIndex";
   public static final String PARAM_ASC = "asc";
   public static final String PARAM_ADDITIONAL_FIELDS = "additionalFields";
+  public static final String PARAM_TIMEZONE = "timeZone";
 
   /**
    * @deprecated since 7.9