@@ -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)); | |||
} | |||
} |
@@ -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) { |
@@ -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"; |
@@ -54,6 +54,15 @@ public final class DateUtils { | |||
return d.toInstant().atZone(ZoneId.systemDefault()).toLocalDate().toString(); | |||
} | |||
/** | |||
* 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! | |||
*/ | |||
@@ -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) { |