From: Julien HENRY Date: Thu, 3 Dec 2020 08:37:58 +0000 (+0100) Subject: SONAR-8427 Add a timeZone param to api/issues/search X-Git-Tag: 8.6.0.39681~17 X-Git-Url: https://source.dussan.org/?a=commitdiff_plain;h=6741d92467d65179e77e910612d91e55b84e660d;p=sonarqube.git SONAR-8427 Add a timeZone param to api/issues/search --- diff --git a/server/sonar-server-common/src/main/java/org/sonar/server/es/Facets.java b/server/sonar-server-common/src/main/java/org/sonar/server/es/Facets.java index 27e7d089ae8..02b6b7e9344 100644 --- a/server/sonar-server-common/src/main/java/org/sonar/server/es/Facets.java +++ b/server/sonar-server-common/src/main/java/org/sonar/server/es/Facets.java @@ -19,13 +19,13 @@ */ 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> facetsByName; - private final TimeZone timeZone; + private final ZoneId timeZone; - public Facets(LinkedHashMap> facetsByName, TimeZone timeZone) { + public Facets(LinkedHashMap> 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) { diff --git a/server/sonar-server-common/src/main/java/org/sonar/server/es/SearchIdResult.java b/server/sonar-server-common/src/main/java/org/sonar/server/es/SearchIdResult.java index c369f0759a9..6783537663f 100644 --- a/server/sonar-server-common/src/main/java/org/sonar/server/es/SearchIdResult.java +++ b/server/sonar-server-common/src/main/java/org/sonar/server/es/SearchIdResult.java @@ -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 { private final Facets facets; private final long total; - public SearchIdResult(SearchResponse response, Function converter, TimeZone timeZone) { + public SearchIdResult(SearchResponse response, Function converter, ZoneId timeZone) { this.facets = new Facets(response, timeZone); this.total = response.getHits().getTotalHits().value; this.uuids = convertToIds(response.getHits(), converter); diff --git a/server/sonar-server-common/src/main/java/org/sonar/server/es/SearchResult.java b/server/sonar-server-common/src/main/java/org/sonar/server/es/SearchResult.java index 996ba9afd1d..7dfd2989359 100644 --- a/server/sonar-server-common/src/main/java/org/sonar/server/es/SearchResult.java +++ b/server/sonar-server-common/src/main/java/org/sonar/server/es/SearchResult.java @@ -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 { private final Facets facets; private final long total; - public SearchResult(SearchResponse response, Function, DOC> converter, TimeZone timeZone) { + public SearchResult(SearchResponse response, Function, DOC> converter, ZoneId timeZone) { this.facets = new Facets(response, timeZone); this.total = response.getHits().getTotalHits().value; this.docs = EsUtils.convertToDocs(response.getHits(), converter); diff --git a/server/sonar-server-common/src/main/java/org/sonar/server/issue/SearchRequest.java b/server/sonar-server-common/src/main/java/org/sonar/server/issue/SearchRequest.java index 3d3ddec151d..4ce3719c363 100644 --- a/server/sonar-server-common/src/main/java/org/sonar/server/issue/SearchRequest.java +++ b/server/sonar-server-common/src/main/java/org/sonar/server/issue/SearchRequest.java @@ -67,6 +67,7 @@ public class SearchRequest { private List sansTop25; private List sonarsourceSecurity; private List 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; + } } diff --git a/server/sonar-server-common/src/main/java/org/sonar/server/rule/index/RuleIndex.java b/server/sonar-server-common/src/main/java/org/sonar/server/rule/index/RuleIndex.java index ead195c2e16..c21619b9464 100644 --- a/server/sonar-server-common/src/main/java/org/sonar/server/rule/index/RuleIndex.java +++ b/server/sonar-server-common/src/main/java/org/sonar/server/rule/index/RuleIndex.java @@ -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()); } /** diff --git a/server/sonar-server-common/src/main/java/org/sonar/server/user/index/UserIndex.java b/server/sonar-server-common/src/main/java/org/sonar/server/user/index/UserIndex.java index 75d142cd6ee..de6f7a0206e 100644 --- a/server/sonar-server-common/src/main/java/org/sonar/server/user/index/UserIndex.java +++ b/server/sonar-server-common/src/main/java/org/sonar/server/user/index/UserIndex.java @@ -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()); } } diff --git a/server/sonar-web/src/main/js/apps/issues/components/AppContainer.tsx b/server/sonar-web/src/main/js/apps/issues/components/AppContainer.tsx index c964c626911..97f04d40be5 100644 --- a/server/sonar-web/src/main/js/apps/issues/components/AppContainer.tsx +++ b/server/sonar-web/src/main/js/apps/issues/components/AppContainer.tsx @@ -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) diff --git a/server/sonar-webserver-core/src/test/java/org/sonar/server/rule/RegisterRulesTest.java b/server/sonar-webserver-core/src/test/java/org/sonar/server/rule/RegisterRulesTest.java index 4425cb5ae79..0227e4908f3 100644 --- a/server/sonar-webserver-core/src/test/java/org/sonar/server/rule/RegisterRulesTest.java +++ b/server/sonar-webserver-core/src/test/java/org/sonar/server/rule/RegisterRulesTest.java @@ -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(); diff --git a/server/sonar-webserver-es/src/main/java/org/sonar/server/component/index/ComponentIndex.java b/server/sonar-webserver-es/src/main/java/org/sonar/server/component/index/ComponentIndex.java index 489e18e758b..886f81583ec 100644 --- a/server/sonar-webserver-es/src/main/java/org/sonar/server/component/index/ComponentIndex.java +++ b/server/sonar-webserver-es/src/main/java/org/sonar/server/component/index/ComponentIndex.java @@ -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) { diff --git a/server/sonar-webserver-es/src/main/java/org/sonar/server/issue/index/IssueIndex.java b/server/sonar-webserver-es/src/main/java/org/sonar/server/issue/index/IssueIndex.java index 1446e48f4ab..0f016ac6c26 100644 --- a/server/sonar-webserver-es/src/main/java/org/sonar/server/issue/index/IssueIndex.java +++ b/server/sonar-webserver-es/src/main/java/org/sonar/server/issue/index/IssueIndex.java @@ -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); diff --git a/server/sonar-webserver-es/src/main/java/org/sonar/server/issue/index/IssueQuery.java b/server/sonar-webserver-es/src/main/java/org/sonar/server/issue/index/IssueQuery.java index c58511323c8..38eb36bb7af 100644 --- a/server/sonar-webserver-es/src/main/java/org/sonar/server/issue/index/IssueQuery.java +++ b/server/sonar-webserver-es/src/main/java/org/sonar/server/issue/index/IssueQuery.java @@ -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 issueKeys() { @@ -294,6 +297,11 @@ public class IssueQuery { return new Builder(); } + @CheckForNull + public ZoneId timeZone() { + return timeZone; + } + public static class Builder { private Collection issueKeys; private Collection 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 Collection defaultCollection(@Nullable Collection c) { diff --git a/server/sonar-webserver-es/src/main/java/org/sonar/server/issue/index/IssueQueryFactory.java b/server/sonar-webserver-es/src/main/java/org/sonar/server/issue/index/IssueQueryFactory.java index 3e93bf5c4ef..65b79e2c509 100644 --- a/server/sonar-webserver-es/src/main/java/org/sonar/server/issue/index/IssueQueryFactory.java +++ b/server/sonar-webserver-es/src/main/java/org/sonar/server/issue/index/IssueQueryFactory.java @@ -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 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 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 componentUuids) { - Date createdAfter = parseStartingDateOrDateTime(request.getCreatedAfter()); + private void setCreatedAfterFromRequest(DbSession dbSession, IssueQuery.Builder builder, SearchRequest request, List componentUuids, ZoneId timeZone) { + Date createdAfter = parseStartingDateOrDateTime(request.getCreatedAfter(), timeZone); String createdInLast = request.getCreatedInLast(); if (request.getSinceLeakPeriod() == null || !request.getSinceLeakPeriod()) { diff --git a/server/sonar-webserver-es/src/main/java/org/sonar/server/measure/index/ProjectMeasuresIndex.java b/server/sonar-webserver-es/src/main/java/org/sonar/server/measure/index/ProjectMeasuresIndex.java index 4231f0d42be..bf81c3a02d7 100644 --- a/server/sonar-webserver-es/src/main/java/org/sonar/server/measure/index/ProjectMeasuresIndex.java +++ b/server/sonar-webserver-es/src/main/java/org/sonar/server/measure/index/ProjectMeasuresIndex.java @@ -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) { diff --git a/server/sonar-webserver-es/src/test/java/org/sonar/server/issue/index/IssueIndexDebtTest.java b/server/sonar-webserver-es/src/test/java/org/sonar/server/issue/index/IssueIndexDebtTest.java index 74d67113b28..35ed6595a36 100644 --- a/server/sonar-webserver-es/src/test/java/org/sonar/server/issue/index/IssueIndexDebtTest.java +++ b/server/sonar-webserver-es/src/test/java/org/sonar/server/issue/index/IssueIndexDebtTest.java @@ -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 createdAt = new Facets(underTest.search(query.build(), searchOptions), system2.getDefaultTimeZone()).get("createdAt"); + Map 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() { diff --git a/server/sonar-webserver-es/src/test/java/org/sonar/server/issue/index/IssueIndexFacetsTest.java b/server/sonar-webserver-es/src/test/java/org/sonar/server/issue/index/IssueIndexFacetsTest.java index 11437d42c90..fb9bbaa04eb 100644 --- a/server/sonar-webserver-es/src/test/java/org/sonar/server/issue/index/IssueIndexFacetsTest.java +++ b/server/sonar-webserver-es/src/test/java/org/sonar/server/issue/index/IssueIndexFacetsTest.java @@ -19,7 +19,10 @@ */ 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 buckets = new Facets(result, system2.getDefaultTimeZone()).get("createdAt"); + Map 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 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 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 createdAt = new Facets(result, system2.getDefaultTimeZone()).get("createdAt"); + Map 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 createdAt = new Facets(result, system2.getDefaultTimeZone()).get("createdAt"); + Map 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 createdAt = new Facets(result, system2.getDefaultTimeZone()).get("createdAt"); + Map 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 createdAt = new Facets(result, system2.getDefaultTimeZone()).get("createdAt"); + Map 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 createdAt = new Facets(result, system2.getDefaultTimeZone()).get("createdAt"); + Map 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 createdAt = new Facets(result, system2.getDefaultTimeZone()).get("createdAt"); + Map 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 createdAt = new Facets(result, system2.getDefaultTimeZone()).get("createdAt"); + Map 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... 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... 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); } } diff --git a/server/sonar-webserver-es/src/test/java/org/sonar/server/issue/index/IssueIndexSecurityHotspotsTest.java b/server/sonar-webserver-es/src/test/java/org/sonar/server/issue/index/IssueIndexSecurityHotspotsTest.java index 054986e98fe..cb5e38dcd29 100644 --- a/server/sonar-webserver-es/src/test/java/org/sonar/server/issue/index/IssueIndexSecurityHotspotsTest.java +++ b/server/sonar-webserver-es/src/test/java/org/sonar/server/issue/index/IssueIndexSecurityHotspotsTest.java @@ -142,7 +142,7 @@ public class IssueIndexSecurityHotspotsTest { @SafeVarargs private final void assertThatFacetHasOnly(IssueQuery.Builder query, String facet, Map.Entry... 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); } diff --git a/server/sonar-webserver-es/src/test/java/org/sonar/server/issue/index/IssueQueryFactoryTest.java b/server/sonar-webserver-es/src/test/java/org/sonar/server/issue/index/IssueQueryFactoryTest.java index 0ebb3805a41..d73c46b97ef 100644 --- a/server/sonar-webserver-es/src/test/java/org/sonar/server/issue/index/IssueQueryFactoryTest.java +++ b/server/sonar-webserver-es/src/test/java/org/sonar/server/issue/index/IssueQueryFactoryTest.java @@ -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(); } diff --git a/server/sonar-webserver-webapi/src/main/java/org/sonar/server/issue/ws/SearchAction.java b/server/sonar-webserver-webapi/src/main/java/org/sonar/server/issue/ws/SearchAction.java index 2ea5eb36b18..6ef83205c23 100644 --- a/server/sonar-webserver-webapi/src/main/java/org/sonar/server/issue/ws/SearchAction.java +++ b/server/sonar-webserver-webapi/src/main/java/org/sonar/server/issue/ws/SearchAction.java @@ -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 { + "
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).
" + - "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.
" + "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).
" + - "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) { diff --git a/server/sonar-webserver-webapi/src/test/java/org/sonar/server/issue/ws/SearchActionTest.java b/server/sonar-webserver-webapi/src/test/java/org/sonar/server/issue/ws/SearchActionTest.java index 8ce84c15a76..8dc98c832a0 100644 --- a/server/sonar-webserver-webapi/src/test/java/org/sonar/server/issue/ws/SearchActionTest.java +++ b/server/sonar-webserver-webapi/src/test/java/org/sonar/server/issue/ws/SearchActionTest.java @@ -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) diff --git a/server/sonar-webserver-webapi/src/test/java/org/sonar/server/rule/RuleCreatorTest.java b/server/sonar-webserver-webapi/src/test/java/org/sonar/server/rule/RuleCreatorTest.java index 569bb2b7abc..77faa4457b4 100644 --- a/server/sonar-webserver-webapi/src/test/java/org/sonar/server/rule/RuleCreatorTest.java +++ b/server/sonar-webserver-webapi/src/test/java/org/sonar/server/rule/RuleCreatorTest.java @@ -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(); diff --git a/server/sonar-webserver-webapi/src/test/java/org/sonar/server/rule/RuleUpdaterTest.java b/server/sonar-webserver-webapi/src/test/java/org/sonar/server/rule/RuleUpdaterTest.java index 8450a557ab8..5b7981176c6 100644 --- a/server/sonar-webserver-webapi/src/test/java/org/sonar/server/rule/RuleUpdaterTest.java +++ b/server/sonar-webserver-webapi/src/test/java/org/sonar/server/rule/RuleUpdaterTest.java @@ -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(); diff --git a/sonar-plugin-api-impl/src/main/java/org/sonar/api/impl/utils/TestSystem2.java b/sonar-plugin-api-impl/src/main/java/org/sonar/api/impl/utils/TestSystem2.java index cc5689a4103..47f2337c714 100644 --- a/sonar-plugin-api-impl/src/main/java/org/sonar/api/impl/utils/TestSystem2.java +++ b/sonar-plugin-api-impl/src/main/java/org/sonar/api/impl/utils/TestSystem2.java @@ -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; diff --git a/sonar-plugin-api/src/main/java/org/sonar/api/utils/DateUtils.java b/sonar-plugin-api/src/main/java/org/sonar/api/utils/DateUtils.java index 2cd20dbb171..4e2704574c5 100644 --- a/sonar-plugin-api/src/main/java/org/sonar/api/utils/DateUtils.java +++ b/sonar-plugin-api/src/main/java/org/sonar/api/utils/DateUtils.java @@ -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()); } /** diff --git a/sonar-plugin-api/src/test/java/org/sonar/api/utils/DateUtilsTest.java b/sonar-plugin-api/src/test/java/org/sonar/api/utils/DateUtilsTest.java index 04283d2ec30..1281b641b07 100644 --- a/sonar-plugin-api/src/test/java/org/sonar/api/utils/DateUtilsTest.java +++ b/sonar-plugin-api/src/test/java/org/sonar/api/utils/DateUtilsTest.java @@ -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 diff --git a/sonar-ws/src/main/java/org/sonarqube/ws/client/issue/IssuesWsParameters.java b/sonar-ws/src/main/java/org/sonarqube/ws/client/issue/IssuesWsParameters.java index 2e81f8c3efa..bca1096d0bd 100644 --- a/sonar-ws/src/main/java/org/sonarqube/ws/client/issue/IssuesWsParameters.java +++ b/sonar-ws/src/main/java/org/sonarqube/ws/client/issue/IssuesWsParameters.java @@ -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