Skip to content

Commit

Permalink
Rebuild default date formats used to parse string as dates when the d…
Browse files Browse the repository at this point in the history
…efault timezone or the lenient flag changes.

Fix #3382
  • Loading branch information
joel-costigliola committed May 23, 2024
1 parent 9eeb352 commit 97b642a
Show file tree
Hide file tree
Showing 4 changed files with 122 additions and 29 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -38,9 +38,11 @@
import java.util.Date;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.TimeZone;
import java.util.concurrent.TimeUnit;
import java.util.function.Function;

import org.assertj.core.configuration.Configuration;
import org.assertj.core.configuration.ConfigurationProvider;
import org.assertj.core.internal.ComparatorBasedComparisonStrategy;
import org.assertj.core.internal.Dates;
Expand Down Expand Up @@ -70,19 +72,33 @@
*/
public abstract class AbstractDateAssert<SELF extends AbstractDateAssert<SELF>> extends AbstractAssert<SELF, Date> {

private static final String DATE_FORMAT_PATTERN_SHOULD_NOT_BE_NULL = "Given date format pattern should not be null";
private static final String DATE_FORMAT_SHOULD_NOT_BE_NULL = "Given date format should not be null";

/**
* the default DateFormat used to parse any String date representation.
*/
private static List<DateFormat> DEFAULT_DATE_FORMATS = defaultDateFormats();
private static boolean lenientParsing = Configuration.LENIENT_DATE_PARSING;

@VisibleForTesting
static final List<DateFormat> DEFAULT_DATE_FORMATS = list(newIsoDateTimeWithMsAndIsoTimeZoneFormat(),
newIsoDateTimeWithMsFormat(),
newTimestampDateFormat(),
newIsoDateTimeWithIsoTimeZoneFormat(),
newIsoDateTimeFormat(),
newIsoDateFormat());
static List<DateFormat> defaultDateFormats() {
if (DEFAULT_DATE_FORMATS == null || defaultDateFormatMustBeRecreated()) {
DEFAULT_DATE_FORMATS = list(newIsoDateTimeWithMsAndIsoTimeZoneFormat(lenientParsing),
newIsoDateTimeWithMsFormat(lenientParsing),
newTimestampDateFormat(lenientParsing),
newIsoDateTimeWithIsoTimeZoneFormat(lenientParsing),
newIsoDateTimeFormat(lenientParsing),
newIsoDateFormat(lenientParsing));
}
return DEFAULT_DATE_FORMATS;
}

private static final String DATE_FORMAT_PATTERN_SHOULD_NOT_BE_NULL = "Given date format pattern should not be null";
private static final String DATE_FORMAT_SHOULD_NOT_BE_NULL = "Given date format should not be null";
private static boolean defaultDateFormatMustBeRecreated() {
// check default timezone or lenient flag changes, only check one date format since all are configured the same way
DateFormat dateFormat = DEFAULT_DATE_FORMATS.get(0);
return !dateFormat.getTimeZone().getID().equals(TimeZone.getDefault().getID()) || dateFormat.isLenient() != lenientParsing;
}

/**
* Used in String based Date assertions - like {@link #isAfter(String)} - to convert input date represented as string
Expand Down Expand Up @@ -3392,13 +3408,10 @@ public SELF withDateFormat(String userCustomDateFormatPattern) {
*
* To revert to default strict date parsing, call {@code setLenientDateParsing(false)}.
*
* @param value whether lenient parsing mode should be enabled or not
* @param lenientDateParsing whether lenient parsing mode should be enabled or not
*/
public static void setLenientDateParsing(boolean value) {
ConfigurationProvider.loadRegisteredConfiguration();
for (DateFormat defaultDateFormat : DEFAULT_DATE_FORMATS) {
defaultDateFormat.setLenient(value);
}
public static void setLenientDateParsing(boolean lenientDateParsing) {
lenientParsing = lenientDateParsing;
}

/**
Expand Down Expand Up @@ -3564,7 +3577,7 @@ public SELF withDefaultDateFormatsOnly() {
}

/**
* Thread safe utility method to parse a Date with {@link #userDateFormats} first, then {@link #DEFAULT_DATE_FORMATS}.
* Thread safe utility method to parse a Date with {@link #userDateFormats} first, then {@link #defaultDateFormats()}.
* <p>
* Returns <code>null</code> if dateAsString parameter is <code>null</code>.
*
Expand All @@ -3587,15 +3600,13 @@ Date parse(String dateAsString) {
info.representation().toStringOf(dateFormatsInOrderOfUsage())));
}

private Date parseDateWithDefaultDateFormats(final String dateAsString) {
synchronized (DEFAULT_DATE_FORMATS) {
return parseDateWith(dateAsString, DEFAULT_DATE_FORMATS);
}
private synchronized Date parseDateWithDefaultDateFormats(final String dateAsString) {
return parseDateWith(dateAsString, defaultDateFormats());
}

private List<DateFormat> dateFormatsInOrderOfUsage() {
List<DateFormat> allDateFormatsInOrderOfUsage = newArrayList(userDateFormats.get());
allDateFormatsInOrderOfUsage.addAll(DEFAULT_DATE_FORMATS);
allDateFormatsInOrderOfUsage.addAll(defaultDateFormats());
return allDateFormatsInOrderOfUsage;
}

Expand Down
74 changes: 66 additions & 8 deletions assertj-core/src/main/java/org/assertj/core/util/DateUtil.java
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@ public class DateUtil {
* @return a {@code yyyy-MM-dd} {@link DateFormat}
*/
public static DateFormat newIsoDateFormat() {
return strictDateFormatForPattern("yyyy-MM-dd");
return newIsoDateFormat(false);
}

/**
Expand All @@ -61,15 +61,15 @@ public static DateFormat newIsoDateFormat() {
* @return a {@code yyyy-MM-dd'T'HH:mm:ssX} {@link DateFormat}
*/
public static DateFormat newIsoDateTimeWithIsoTimeZoneFormat() {
return strictDateFormatForPattern("yyyy-MM-dd'T'HH:mm:ssX");
return newIsoDateTimeWithIsoTimeZoneFormat(false);
}

/**
* ISO 8601 date-time format (yyyy-MM-dd'T'HH:mm:ss), example : <code>2003-04-26T13:01:02</code>
* @return a {@code yyyy-MM-dd'T'HH:mm:ss} {@link DateFormat}
*/
public static DateFormat newIsoDateTimeFormat() {
return strictDateFormatForPattern("yyyy-MM-dd'T'HH:mm:ss");
return newIsoDateTimeFormat(false);
}

/**
Expand All @@ -78,7 +78,7 @@ public static DateFormat newIsoDateTimeFormat() {
* @return a {@code yyyy-MM-dd'T'HH:mm:ss.SSS} {@link DateFormat}
*/
public static DateFormat newIsoDateTimeWithMsFormat() {
return strictDateFormatForPattern("yyyy-MM-dd'T'HH:mm:ss.SSS");
return newIsoDateTimeWithMsFormat(false);
}

/**
Expand All @@ -87,7 +87,7 @@ public static DateFormat newIsoDateTimeWithMsFormat() {
* @return a {@code yyyy-MM-dd'T'HH:mm:ss.SSSX} {@link DateFormat}
*/
public static DateFormat newIsoDateTimeWithMsAndIsoTimeZoneFormat() {
return strictDateFormatForPattern("yyyy-MM-dd'T'HH:mm:ss.SSSX");
return newIsoDateTimeWithMsAndIsoTimeZoneFormat(false);
}

/**
Expand All @@ -96,12 +96,70 @@ public static DateFormat newIsoDateTimeWithMsAndIsoTimeZoneFormat() {
* @return a {@code yyyy-MM-dd HH:mm:ss.SSS} {@link DateFormat}
*/
public static DateFormat newTimestampDateFormat() {
return strictDateFormatForPattern("yyyy-MM-dd HH:mm:ss.SSS");
return newTimestampDateFormat(false);
}

private static DateFormat strictDateFormatForPattern(String pattern) {
/**
* ISO 8601 date format (yyyy-MM-dd), example : <code>2003-04-23</code>
* @param lenientParsing whether or not parsing the date is lenient
* @return a {@code yyyy-MM-dd} {@link DateFormat}
*/
public static DateFormat newIsoDateFormat(boolean lenientParsing) {
return dateFormatForPattern("yyyy-MM-dd", lenientParsing);
}

/**
* ISO 8601 date-time format with ISO time zone (yyyy-MM-dd'T'HH:mm:ssX), example :
* <code>2003-04-26T03:01:02+00:00</code>
* @param lenientParsing whether or not parsing the date is lenient
* @return a {@code yyyy-MM-dd'T'HH:mm:ssX} {@link DateFormat}
*/
public static DateFormat newIsoDateTimeWithIsoTimeZoneFormat(boolean lenientParsing) {
return dateFormatForPattern("yyyy-MM-dd'T'HH:mm:ssX", lenientParsing);
}

/**
* ISO 8601 date-time format (yyyy-MM-dd'T'HH:mm:ss), example : <code>2003-04-26T13:01:02</code>
* @param lenientParsing whether or not parsing the date is lenient
* @return a {@code yyyy-MM-dd'T'HH:mm:ss} {@link DateFormat}
*/
public static DateFormat newIsoDateTimeFormat(boolean lenientParsing) {
return dateFormatForPattern("yyyy-MM-dd'T'HH:mm:ss", lenientParsing);
}

/**
* ISO 8601 date-time format with millisecond (yyyy-MM-dd'T'HH:mm:ss.SSS), example :
* <code>2003-04-26T03:01:02.999</code>
* @param lenientParsing whether or not parsing the date is lenient
* @return a {@code yyyy-MM-dd'T'HH:mm:ss.SSS} {@link DateFormat}
*/
public static DateFormat newIsoDateTimeWithMsFormat(boolean lenientParsing) {
return dateFormatForPattern("yyyy-MM-dd'T'HH:mm:ss.SSS", lenientParsing);
}

/**
* ISO 8601 date-time format with millisecond and ISO time zone (yyyy-MM-dd'T'HH:mm:ss.SSSX), example :
* <code>2003-04-26T03:01:02.758+00:00</code>
* @param lenientParsing whether or not parsing the date is lenient
* @return a {@code yyyy-MM-dd'T'HH:mm:ss.SSSX} {@link DateFormat}
*/
public static DateFormat newIsoDateTimeWithMsAndIsoTimeZoneFormat(boolean lenientParsing) {
return dateFormatForPattern("yyyy-MM-dd'T'HH:mm:ss.SSSX", lenientParsing);
}

/**
* {@link java.sql.Timestamp} date-time format with millisecond (yyyy-MM-dd HH:mm:ss.SSS), example :
* <code>2003-04-26 03:01:02.999</code>
* @param lenientParsing whether or not parsing the date is lenient
* @return a {@code yyyy-MM-dd HH:mm:ss.SSS} {@link DateFormat}
*/
public static DateFormat newTimestampDateFormat(boolean lenientParsing) {
return dateFormatForPattern("yyyy-MM-dd HH:mm:ss.SSS", lenientParsing);
}

private static DateFormat dateFormatForPattern(String pattern, boolean lenient) {
DateFormat dateFormat = new SimpleDateFormat(pattern);
dateFormat.setLenient(false);
dateFormat.setLenient(lenient);
return dateFormat;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ void should_setLenientDateParsing(Consumer<Boolean> setLenientDateParsingFunctio
// WHEN
setLenientDateParsingFunction.accept(true);
// THEN
then(AbstractDateAssert.DEFAULT_DATE_FORMATS).allMatch(DateFormat::isLenient);
then(AbstractDateAssert.defaultDateFormats()).allMatch(DateFormat::isLenient);
}

private static Stream<Consumer<Boolean>> setLenientDateParsingFunctions() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,17 +24,20 @@

import java.sql.Timestamp;
import java.text.SimpleDateFormat;
import java.time.Instant;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.OffsetDateTime;
import java.time.ZoneId;
import java.time.ZoneOffset;
import java.util.Date;
import java.util.TimeZone;
import java.util.concurrent.TimeUnit;

import org.assertj.core.api.DateAssertBaseTest;
import org.assertj.core.util.DateUtil;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;

/**
Expand All @@ -44,10 +47,20 @@
*/
class DateAssert_with_string_based_date_representation_Test extends DateAssertBaseTest {

private TimeZone defaultTimeZone;

@Override
@BeforeEach
public void setUp() {
super.setUp();
defaultTimeZone = TimeZone.getDefault();
}

@Override
@AfterEach
public void tearDown() {
useDefaultDateFormatsOnly();
TimeZone.setDefault(defaultTimeZone);
}

@Test
Expand Down Expand Up @@ -256,4 +269,15 @@ void use_custom_date_formats_first_then_defaults_to_parse_a_date() {
assertThat(date).isEqualTo("2003 04 26");
}

@Test
void default_date_formats_should_support_default_timezone_change() {
// GIVEN
TimeZone.setDefault(TimeZone.getTimeZone("CET"));
// need to call a date assertion to initialize the default date formats before changing the timezone.
assertThat(Date.from(Instant.parse("2024-03-01T00:00:00.000+01:00"))).as("In CET time zone").isEqualTo("2024-03-01");
// WHEN
TimeZone.setDefault(TimeZone.getTimeZone("WET"));
// THEN
then(Date.from(Instant.parse("2024-03-01T00:00:00.000+00:00"))).as("In WET time zone").isEqualTo("2024-03-01");
}
}

0 comments on commit 97b642a

Please sign in to comment.