diff options
4 files changed, 98 insertions, 110 deletions
diff --git a/server/sonar-ce-task-projectanalysis/src/main/java/org/sonar/ce/task/projectanalysis/step/LoadPeriodsStep.java b/server/sonar-ce-task-projectanalysis/src/main/java/org/sonar/ce/task/projectanalysis/step/LoadPeriodsStep.java index 3d6c5e271a5..9070f7a2ddf 100644 --- a/server/sonar-ce-task-projectanalysis/src/main/java/org/sonar/ce/task/projectanalysis/step/LoadPeriodsStep.java +++ b/server/sonar-ce-task-projectanalysis/src/main/java/org/sonar/ce/task/projectanalysis/step/LoadPeriodsStep.java @@ -19,12 +19,13 @@ */ package org.sonar.ce.task.projectanalysis.step; +import java.time.Duration; +import java.time.Instant; import java.time.LocalDate; import java.time.ZoneId; import java.time.format.DateTimeParseException; import java.time.temporal.ChronoUnit; import java.util.Arrays; -import java.util.Date; import java.util.List; import java.util.Optional; import java.util.function.Supplier; @@ -129,7 +130,7 @@ public class LoadPeriodsStep implements ComputationStep { if (days != null) { return resolveByDays(dbSession, projectUuid, days, propertyValue); } - Date date = parseDate(propertyValue); + Instant date = parseDate(propertyValue); if (date != null) { return resolveByDate(dbSession, projectUuid, date, propertyValue); } @@ -141,12 +142,12 @@ public class LoadPeriodsStep implements ComputationStep { String mostRecentVersion = Optional.ofNullable(versions.iterator().next().getName()) .orElseThrow(() -> new IllegalStateException("selectVersionsByMostRecentFirst returned a DTO which didn't have a name")); - if (versions.size() == 1) { - return resolveWhenOnlyOneExistingVersion(dbSession, projectUuid, mostRecentVersion, propertyValue); - } boolean previousVersionPeriod = LEAK_PERIOD_MODE_PREVIOUS_VERSION.equals(propertyValue); if (previousVersionPeriod) { + if (versions.size() == 1) { + return resolvePreviousVersionWithOnlyOneExistingVersion(dbSession, projectUuid); + } return resolvePreviousVersion(dbSession, analysisProjectVersion, versions, mostRecentVersion); } @@ -154,10 +155,10 @@ public class LoadPeriodsStep implements ComputationStep { } @CheckForNull - private static Date parseDate(String propertyValue) { + private static Instant parseDate(String propertyValue) { try { LocalDate localDate = LocalDate.parse(propertyValue); - return Date.from(localDate.atStartOfDay(ZoneId.systemDefault()).toInstant()); + return localDate.atStartOfDay(ZoneId.systemDefault()).toInstant(); } catch (DateTimeParseException e) { boolean invalidDate = e.getCause() == null || e.getCause() == e || !e.getCause().getMessage().contains("Invalid date"); checkPeriodProperty(invalidDate, propertyValue, "Invalid date"); @@ -171,24 +172,23 @@ public class LoadPeriodsStep implements ComputationStep { List<SnapshotDto> snapshots = dbClient.snapshotDao().selectAnalysesByQuery(dbSession, createCommonQuery(projectUuid).setCreatedBefore(analysisDate).setSort(BY_DATE, ASC)); ensureNotOnFirstAnalysis(!snapshots.isEmpty()); - long targetDate = DateUtils.addDays(new Date(analysisDate), -days).getTime(); - LOG.debug("Resolving new code period by {} days: {}", days, async(() -> logDate(targetDate))); + Instant targetDate = DateUtils.addDays(Instant.ofEpochMilli(analysisDate), -days); + LOG.debug("Resolving new code period by {} days: {}", days, supplierToString(() -> logDate(targetDate))); SnapshotDto snapshot = findNearestSnapshotToTargetDate(snapshots, targetDate); return Optional.of(newPeriod(LEAK_PERIOD_MODE_DAYS, String.valueOf((int) days), snapshot)); } - private Optional<Period> resolveByDate(DbSession dbSession, String projectUuid, Date date, String propertyValue) { - long now = system2.now(); - checkPeriodProperty(date.compareTo(new Date(now)) <= 0, propertyValue, - "date is in the future (now: '%s')", async(() -> logDate(now))); + private Optional<Period> resolveByDate(DbSession dbSession, String projectUuid, Instant date, String propertyValue) { + Instant now = Instant.ofEpochMilli(system2.now()); + checkPeriodProperty(date.compareTo(now) <= 0, propertyValue, + "date is in the future (now: '%s')", supplierToString(() -> logDate(now))); - LOG.debug("Resolving new code period by date: {}", async(() -> logDate(date))); - Optional<Period> period = findFirstSnapshot(dbSession, createCommonQuery(projectUuid).setCreatedAfter(date.getTime()).setSort(BY_DATE, ASC)) + LOG.debug("Resolving new code period by date: {}", supplierToString(() -> logDate(date))); + Optional<Period> period = findFirstSnapshot(dbSession, createCommonQuery(projectUuid).setCreatedAfter(date.toEpochMilli()).setSort(BY_DATE, ASC)) .map(dto -> newPeriod(LEAK_PERIOD_MODE_DATE, DateUtils.formatDate(date), dto)); - checkPeriodProperty(period.isPresent(), propertyValue, "No analysis found created after date '%s'", async(() -> logDate(date))); - + checkPeriodProperty(period.isPresent(), propertyValue, "No analysis found created after date '%s'", supplierToString(() -> logDate(date))); return period; } @@ -205,15 +205,8 @@ public class LoadPeriodsStep implements ComputationStep { return findOldestAnalysis(dbSession, periodMode, projectUuid); } - private Optional<Period> resolveWhenOnlyOneExistingVersion(DbSession dbSession, String projectUuid, String mostRecentVersion, String propertyValue) { - boolean previousVersionPeriod = LEAK_PERIOD_MODE_PREVIOUS_VERSION.equals(propertyValue); + private Optional<Period> resolvePreviousVersionWithOnlyOneExistingVersion(DbSession dbSession, String projectUuid) { LOG.debug("Resolving first analysis as new code period as there is only one existing version"); - - // only one existing version. Period must either be PREVIOUS_VERSION or the only valid version: the only existing one - checkPeriodProperty(previousVersionPeriod || propertyValue.equals(mostRecentVersion), propertyValue, - "Only one existing version, but period is neither %s nor this one version '%s' (actual: '%s')", - LEAK_PERIOD_MODE_PREVIOUS_VERSION, mostRecentVersion, propertyValue); - return findOldestAnalysis(dbSession, LEAK_PERIOD_MODE_PREVIOUS_VERSION, projectUuid); } @@ -234,7 +227,7 @@ public class LoadPeriodsStep implements ComputationStep { LOG.debug("Resolving new code period by version: {}", propertyValue); Optional<EventDto> version = versions.stream().filter(t -> propertyValue.equals(t.getName())).findFirst(); checkPeriodProperty(version.isPresent(), propertyValue, - "version is none of the existing ones: %s", async(() -> toVersions(versions))); + "version is none of the existing ones: %s", supplierToString(() -> toVersions(versions))); return newPeriod(dbSession, LEAK_PERIOD_MODE_VERSION, version.get()); } @@ -253,7 +246,7 @@ public class LoadPeriodsStep implements ComputationStep { return Arrays.toString(versions.stream().map(EventDto::getName).toArray(String[]::new)); } - public static Object async(Supplier<String> s) { + private static Object supplierToString(Supplier<String> s) { return new Object() { @Override public String toString() { @@ -268,7 +261,7 @@ public class LoadPeriodsStep implements ComputationStep { private static void checkPeriodProperty(boolean test, String propertyValue, String testDescription, Object... args) { if (!test) { - LOG.debug("Invalid code period '{}': {}", propertyValue, async(() -> format(testDescription, args))); + LOG.debug("Invalid code period '{}': {}", propertyValue, supplierToString(() -> format(testDescription, args))); throw MessageException.of(format("Invalid new code period. '%s' is not one of: " + "integer > 0, date before current analysis j, \"previous_version\", or version string that exists in the project' \n" + "Please contact a project administrator to correct this setting", propertyValue)); @@ -295,13 +288,15 @@ public class LoadPeriodsStep implements ComputationStep { } } - private static SnapshotDto findNearestSnapshotToTargetDate(List<SnapshotDto> snapshots, Long targetDate) { - long bestDistance = Long.MAX_VALUE; + private static SnapshotDto findNearestSnapshotToTargetDate(List<SnapshotDto> snapshots, Instant targetDate) { + // FIXME shouldn't this be the first analysis after targetDate? + Duration bestDuration = null; SnapshotDto nearest = null; for (SnapshotDto snapshot : snapshots) { - long distance = Math.abs(snapshot.getCreatedAt() - targetDate); - if (distance <= bestDistance) { - bestDistance = distance; + Instant createdAt = Instant.ofEpochMilli(snapshot.getCreatedAt()); + Duration duration = Duration.between(targetDate, createdAt).abs(); + if (bestDuration == null || duration.compareTo(bestDuration) <= 0) { + bestDuration = duration; nearest = snapshot; } } @@ -312,11 +307,7 @@ public class LoadPeriodsStep implements ComputationStep { return new SnapshotQuery().setComponentUuid(projectUuid).setStatus(STATUS_PROCESSED); } - private static String logDate(long date) { - return logDate(new Date(date)); - } - - private static String logDate(Date date1) { - return DateUtils.formatDate(Date.from(date1.toInstant().truncatedTo(ChronoUnit.SECONDS))); + private static String logDate(Instant instant) { + return DateUtils.formatDate(instant.truncatedTo(ChronoUnit.SECONDS)); } } diff --git a/server/sonar-ce-task-projectanalysis/src/test/java/org/sonar/ce/task/projectanalysis/step/LoadPeriodsStepTest.java b/server/sonar-ce-task-projectanalysis/src/test/java/org/sonar/ce/task/projectanalysis/step/LoadPeriodsStepTest.java index d62086ef42e..8ac7cb76f37 100644 --- a/server/sonar-ce-task-projectanalysis/src/test/java/org/sonar/ce/task/projectanalysis/step/LoadPeriodsStepTest.java +++ b/server/sonar-ce-task-projectanalysis/src/test/java/org/sonar/ce/task/projectanalysis/step/LoadPeriodsStepTest.java @@ -28,6 +28,7 @@ import java.util.Arrays; import java.util.Date; import java.util.Random; import java.util.stream.Stream; +import javax.annotation.Nullable; import org.apache.commons.lang.RandomStringUtils; import org.junit.Before; import org.junit.Rule; @@ -138,19 +139,15 @@ public class LoadPeriodsStepTest extends BaseStepTest { settings.setProperty("sonar.leak.period", textDate); underTest.execute(new TestComputationStepContext()); - Period period = periodsHolder.getPeriod(); - assertThat(period).isNotNull(); - assertThat(period.getMode()).isEqualTo(LEAK_PERIOD_MODE_DATE); - assertThat(period.getModeParameter()).isEqualTo(textDate); - assertThat(period.getSnapshotDate()).isEqualTo(analysis.getCreatedAt()); - assertThat(period.getAnalysisUuid()).isEqualTo(analysis.getUuid()); + assertPeriod(LEAK_PERIOD_MODE_DATE, textDate, analysis.getCreatedAt(), analysis.getUuid()); } @Test public void ignore_unprocessed_snapshots() { OrganizationDto organization = dbTester.organizations().insert(); ComponentDto project = dbTester.components().insertPrivateProject(organization); - SnapshotDto analysis1 = dbTester.components().insertSnapshot(project, snapshot -> snapshot.setStatus(STATUS_UNPROCESSED).setCreatedAt(1226379600000L).setLast(false));// 2008-11-11 + SnapshotDto analysis1 = dbTester.components() + .insertSnapshot(project, snapshot -> snapshot.setStatus(STATUS_UNPROCESSED).setCreatedAt(1226379600000L).setLast(false));// 2008-11-11 SnapshotDto analysis2 = dbTester.components().insertSnapshot(project, snapshot -> snapshot.setStatus(STATUS_PROCESSED).setVersion("not provided").setCreatedAt(1226379600000L).setLast(false));// 2008-11-29 dbTester.events().insertEvent(newEvent(analysis1).setName("not provided").setCategory(CATEGORY_VERSION).setDate(analysis1.getCreatedAt())); @@ -162,13 +159,7 @@ public class LoadPeriodsStepTest extends BaseStepTest { settings.setProperty("sonar.leak.period", "100"); underTest.execute(new TestComputationStepContext()); - Period period = periodsHolder.getPeriod(); - assertThat(period).isNotNull(); - assertThat(period.getMode()).isEqualTo(LEAK_PERIOD_MODE_DAYS); - assertThat(period.getModeParameter()).isEqualTo("100"); - assertThat(period.getSnapshotDate()).isEqualTo(analysis2.getCreatedAt()); - assertThat(period.getAnalysisUuid()).isEqualTo(analysis2.getUuid()); - + assertPeriod(LEAK_PERIOD_MODE_DAYS, "100", analysis2.getCreatedAt(), analysis2.getUuid()); verifyDebugLogs("Resolving new code period by 100 days: 2008-08-22"); } @@ -190,12 +181,7 @@ public class LoadPeriodsStepTest extends BaseStepTest { underTest.execute(new TestComputationStepContext()); // Return analysis from given date 2008-11-22 - Period period = periodsHolder.getPeriod(); - assertThat(period).isNotNull(); - assertThat(period.getMode()).isEqualTo(LEAK_PERIOD_MODE_DATE); - assertThat(period.getModeParameter()).isEqualTo(textDate); - assertThat(period.getSnapshotDate()).isEqualTo(analysis4.getCreatedAt()); - assertThat(period.getAnalysisUuid()).isEqualTo(analysis4.getUuid()); + assertPeriod(LEAK_PERIOD_MODE_DATE, textDate, analysis4.getCreatedAt(), analysis4.getUuid()); verifyDebugLogs("Resolving new code period by date: 2008-11-22"); } @@ -218,13 +204,7 @@ public class LoadPeriodsStepTest extends BaseStepTest { underTest.execute(new TestComputationStepContext()); // Analysis form 2008-11-20 - Period period = periodsHolder.getPeriod(); - assertThat(period).isNotNull(); - assertThat(period.getMode()).isEqualTo(LEAK_PERIOD_MODE_DATE); - assertThat(period.getModeParameter()).isEqualTo(date); - assertThat(period.getSnapshotDate()).isEqualTo(analysis3.getCreatedAt()); - assertThat(period.getAnalysisUuid()).isEqualTo(analysis3.getUuid()); - + assertPeriod(LEAK_PERIOD_MODE_DATE, date, analysis3.getCreatedAt(), analysis3.getUuid()); verifyDebugLogs("Resolving new code period by date: 2008-11-13"); } @@ -415,12 +395,7 @@ public class LoadPeriodsStepTest extends BaseStepTest { underTest.execute(new TestComputationStepContext()); // return analysis from 2008-11-20 - Period period = periodsHolder.getPeriod(); - assertThat(period).isNotNull(); - assertThat(period.getMode()).isEqualTo(LEAK_PERIOD_MODE_DAYS); - assertThat(period.getModeParameter()).isEqualTo("10"); - assertThat(period.getSnapshotDate()).isEqualTo(analysis3.getCreatedAt()); - assertThat(period.getAnalysisUuid()).isEqualTo(analysis3.getUuid()); + assertPeriod(LEAK_PERIOD_MODE_DAYS, "10", analysis3.getCreatedAt(), analysis3.getUuid()); assertThat(logTester.getLogs()).hasSize(1); assertThat(logTester.getLogs(LoggerLevel.DEBUG)) @@ -448,12 +423,7 @@ public class LoadPeriodsStepTest extends BaseStepTest { underTest.execute(new TestComputationStepContext()); // Analysis form 2008-11-12 - Period period = periodsHolder.getPeriod(); - assertThat(period).isNotNull(); - assertThat(period.getMode()).isEqualTo(LEAK_PERIOD_MODE_PREVIOUS_VERSION); - assertThat(period.getModeParameter()).isEqualTo("1.0"); - assertThat(period.getSnapshotDate()).isEqualTo(analysis2.getCreatedAt()); - assertThat(period.getAnalysisUuid()).isEqualTo(analysis2.getUuid()); + assertPeriod(LEAK_PERIOD_MODE_PREVIOUS_VERSION, "1.0", analysis2.getCreatedAt(), analysis2.getUuid()); verifyDebugLogs("Resolving new code period by previous version: 1.0"); } @@ -476,12 +446,7 @@ public class LoadPeriodsStepTest extends BaseStepTest { underTest.execute(new TestComputationStepContext()); // Analysis form 2008-11-11 - Period period = periodsHolder.getPeriod(); - assertThat(period).isNotNull(); - assertThat(period.getMode()).isEqualTo(LEAK_PERIOD_MODE_PREVIOUS_VERSION); - assertThat(period.getModeParameter()).isEqualTo("0.9"); - assertThat(period.getSnapshotDate()).isEqualTo(analysis1.getCreatedAt()); - assertThat(period.getAnalysisUuid()).isEqualTo(analysis1.getUuid()); + assertPeriod(LEAK_PERIOD_MODE_PREVIOUS_VERSION, "0.9", analysis1.getCreatedAt(), analysis1.getUuid()); } @Test @@ -498,12 +463,7 @@ public class LoadPeriodsStepTest extends BaseStepTest { settings.setProperty("sonar.leak.period", "previous_version"); underTest.execute(new TestComputationStepContext()); - Period period = periodsHolder.getPeriod(); - assertThat(period).isNotNull(); - assertThat(period.getMode()).isEqualTo(LEAK_PERIOD_MODE_PREVIOUS_VERSION); - assertThat(period.getModeParameter()).isNull(); - assertThat(period.getSnapshotDate()).isEqualTo(analysis1.getCreatedAt()); - assertThat(period.getAnalysisUuid()).isEqualTo(analysis1.getUuid()); + assertPeriod(LEAK_PERIOD_MODE_PREVIOUS_VERSION, null, analysis1.getCreatedAt(), analysis1.getUuid()); verifyDebugLogs("Resolving first analysis as new code period as there is only one existing version"); } @@ -521,13 +481,7 @@ public class LoadPeriodsStepTest extends BaseStepTest { settings.setProperty("sonar.leak.period", "previous_version"); underTest.execute(new TestComputationStepContext()); - Period period = periodsHolder.getPeriod(); - assertThat(period).isNotNull(); - assertThat(period.getMode()).isEqualTo(LEAK_PERIOD_MODE_PREVIOUS_VERSION); - assertThat(period.getModeParameter()).isNull(); - assertThat(period.getSnapshotDate()).isEqualTo(analysis.getCreatedAt()); - assertThat(period.getAnalysisUuid()).isEqualTo(analysis.getUuid()); - + assertPeriod(LEAK_PERIOD_MODE_PREVIOUS_VERSION, null, analysis.getCreatedAt(), analysis.getUuid()); verifyDebugLogs("Resolving first analysis as new code period as there is only one existing version"); } @@ -551,14 +505,38 @@ public class LoadPeriodsStepTest extends BaseStepTest { underTest.execute(new TestComputationStepContext()); // Analysis form 2008-11-11 + assertPeriod(LEAK_PERIOD_MODE_VERSION, "1.0", analysis2.getCreatedAt(), analysis2.getUuid()); + verifyDebugLogs("Resolving new code period by version: 1.0"); + } + + /** + * SONAR-11492 + */ + @Test + public void feed_period_by_version_with_only_one_existing_version() { + OrganizationDto organization = dbTester.organizations().insert(); + ComponentDto project = dbTester.components().insertPrivateProject(organization); + SnapshotDto analysis1 = dbTester.components().insertSnapshot(project, snapshot -> snapshot.setCreatedAt(1226379600000L).setVersion("0.9").setLast(true)); // 2008-11-11 + dbTester.events().insertEvent(newEvent(analysis1).setName("0.9").setCategory(CATEGORY_VERSION)); + when(system2Mock.now()).thenReturn(november30th2008.getTime()); + when(analysisMetadataHolder.isFirstAnalysis()).thenReturn(false); + setupRoot(project, "0.9"); + + settings.setProperty("sonar.leak.period", "0.9"); + underTest.execute(new TestComputationStepContext()); + + // Analysis form 2008-11-11 + assertPeriod(LEAK_PERIOD_MODE_VERSION, "0.9", analysis1.getCreatedAt(), analysis1.getUuid()); + verifyDebugLogs("Resolving new code period by version: 0.9"); + } + + private void assertPeriod(String mode, @Nullable String modeParameter, long snapshotDate, String analysisUuid) { Period period = periodsHolder.getPeriod(); assertThat(period).isNotNull(); - assertThat(period.getMode()).isEqualTo(LEAK_PERIOD_MODE_VERSION); - assertThat(period.getModeParameter()).isEqualTo("1.0"); - assertThat(period.getSnapshotDate()).isEqualTo(analysis2.getCreatedAt()); - assertThat(period.getAnalysisUuid()).isEqualTo(analysis2.getUuid()); - - verifyDebugLogs("Resolving new code period by version: 1.0"); + assertThat(period.getMode()).isEqualTo(mode); + assertThat(period.getModeParameter()).isEqualTo(modeParameter); + assertThat(period.getSnapshotDate()).isEqualTo(snapshotDate); + assertThat(period.getAnalysisUuid()).isEqualTo(analysisUuid); } private void verifyDebugLogs(String log, String... otherLogs) { diff --git a/sonar-core/src/main/java/org/sonar/core/config/CorePropertyDefinitions.java b/sonar-core/src/main/java/org/sonar/core/config/CorePropertyDefinitions.java index 80ebf672994..c30758dc895 100644 --- a/sonar-core/src/main/java/org/sonar/core/config/CorePropertyDefinitions.java +++ b/sonar-core/src/main/java/org/sonar/core/config/CorePropertyDefinitions.java @@ -33,7 +33,6 @@ import static org.sonar.api.PropertyType.BOOLEAN; public class CorePropertyDefinitions { public static final String LEAK_PERIOD = "sonar.leak.period"; - public static final String LEAK_PERIOD_MODE_PREVIOUS_ANALYSIS = "previous_analysis"; public static final String LEAK_PERIOD_MODE_DATE = "date"; public static final String LEAK_PERIOD_MODE_VERSION = "version"; public static final String LEAK_PERIOD_MODE_DAYS = "days"; 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 53e8ec65c10..aa9a8bea943 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 @@ -56,6 +56,15 @@ public final class DateUtils { /** * Warning: relies on default timezone! + * + * @since 7.6 + */ + public static String formatDate(Instant d) { + return d.atZone(ZoneId.systemDefault()).toLocalDate().toString(); + } + + /** + * Warning: relies on default timezone! */ public static String formatDateTime(Date d) { return formatDateTime(OffsetDateTime.ofInstant(d.toInstant(), ZoneId.systemDefault())); @@ -94,6 +103,7 @@ public final class DateUtils { /** * Return a date at the start of day. + * * @param s string in format {@link #DATE_FORMAT} * @throws SonarException when string cannot be parsed */ @@ -218,8 +228,9 @@ public final class DateUtils { /** * Warning: may rely on default timezone! - * @throws IllegalArgumentException if stringDate is not a correctly formed date or datetime + * * @return the datetime, {@code null} if stringDate is null + * @throws IllegalArgumentException if stringDate is not a correctly formed date or datetime * @since 6.1 */ @CheckForNull @@ -241,7 +252,8 @@ public final class DateUtils { /** * Warning: may rely on default timezone! - * @see #parseDateOrDateTime(String) + * + * @see #parseDateOrDateTime(String) */ @CheckForNull public static Date parseStartingDateOrDateTime(@Nullable String stringDate) { @@ -251,9 +263,10 @@ 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) - * @throws IllegalArgumentException if stringDate is not a correctly formed date or datetime + * * @return the datetime, {@code null} if stringDate is null + * @throws IllegalArgumentException if stringDate is not a correctly formed date or datetime + * @see #parseDateOrDateTime(String) * @since 6.1 */ @CheckForNull @@ -277,14 +290,21 @@ public final class DateUtils { * Adds a number of days to a date returning a new object. * The original date object is unchanged. * - * @param date the date, not null - * @param numberOfDays the amount to add, may be negative + * @param date the date, not null + * @param numberOfDays the amount to add, may be negative * @return the new date object with the amount added */ public static Date addDays(Date date, int numberOfDays) { return Date.from(date.toInstant().plus(numberOfDays, ChronoUnit.DAYS)); } + /** + * @since 7.6 + */ + public static Instant addDays(Instant instant, int numberOfDays) { + return instant.plus(numberOfDays, ChronoUnit.DAYS); + } + @CheckForNull public static Date truncateToSeconds(@Nullable Date d) { if (d == null) { |