diff options
4 files changed, 461 insertions, 10 deletions
diff --git a/server/src/main/java/com/vaadin/ui/AbstractDateField.java b/server/src/main/java/com/vaadin/ui/AbstractDateField.java index 9c5d9bb656..84a26436c0 100644 --- a/server/src/main/java/com/vaadin/ui/AbstractDateField.java +++ b/server/src/main/java/com/vaadin/ui/AbstractDateField.java @@ -179,6 +179,30 @@ public abstract class AbstractDateField<T extends Temporal & TemporalAdjuster & }; /** + * The default start year (inclusive) from which to calculate the + * daylight-saving time zone transition dates. + */ + private static final int DEFAULT_START_YEAR = 1980; + + /** + * The default value of the number of future years from the current date for + * which the daylight-saving time zone transition dates are calculated. + */ + private static final int DEFAULT_YEARS_FROM_NOW = 20; + + /** + * The optional user-supplied start year (inclusive) from which to calculate + * the daylight-saving time zone transition dates. + */ + private Integer startYear; + + /** + * The optional user-supplied end year (inclusive) until which to calculate + * the daylight-saving time zone transition dates. + */ + private Integer endYear; + + /** * Value of the field. */ private T value; @@ -489,7 +513,7 @@ public abstract class AbstractDateField<T extends Temporal & TemporalAdjuster & /** * Sets the {@link ZoneId}, which is used when {@code z} is included inside - * the {@link #setDateFormat(String)}. + * the {@link #setDateFormat(String)} . * * @param zoneId * the zone id @@ -498,27 +522,82 @@ public abstract class AbstractDateField<T extends Temporal & TemporalAdjuster & public void setZoneId(ZoneId zoneId) { if (zoneId != this.zoneId || (zoneId != null && !zoneId.equals(this.zoneId))) { - updateTimeZoneJSON(zoneId, getLocale()); + updateTimeZoneJSON(zoneId, getLocale(), getStartYear(), + getEndYear()); } this.zoneId = zoneId; } - private void updateTimeZoneJSON(ZoneId zoneId, Locale locale) { + private void updateTimeZoneJSON(ZoneId zoneId, Locale locale, int startYear, + int endYear) { String timeZoneJSON; if (zoneId != null && locale != null) { - timeZoneJSON = TimeZoneUtil.toJSON(zoneId, locale); + timeZoneJSON = TimeZoneUtil.toJSON(zoneId, locale, startYear, + endYear); } else { timeZoneJSON = null; } getState().timeZoneJSON = timeZoneJSON; } + /** + * Sets {@link startYear} and {@link endYear}: the start and end years (both + * inclusive) between which to calculate the daylight-saving time zone + * transition dates. Both parameters are used when '{@code z}' is included + * inside the {@link #setDateFormat(String)}, they would have no effect + * otherwise. Specifically, these parameters determine the range of years in + * which zone names are are adjusted to show the daylight saving names. + * + * If no values are provided, by default {@link startYear} is set to + * {@value #DEFAULT_START_YEAR}, and {@link endYear} is set to + * {@value #DEFAULT_YEARS_FROM_NOW} years into the future from the current + * date. + * + * @param startYear + * the start year of DST transitions + * @param endYear + * the end year of DST transitions + * @since 8.11 + */ + public void setDaylightSavingTimeRange(int startYear, int endYear) { + if (startYear > endYear) { + throw new IllegalArgumentException( + "The start year from which to begin calculating the " + + "daylight-saving time zone transition dates must" + + " be less than or equal to the end year.\n" + + startYear + " is greater than " + endYear); + } + if (this.startYear == null || this.endYear == null + || startYear != this.startYear || endYear != this.endYear) { + updateTimeZoneJSON(getZoneId(), getLocale(), startYear, endYear); + } + this.startYear = startYear; + this.endYear = endYear; + } + + private int getStartYear() { + if (startYear == null) { + return DEFAULT_START_YEAR; + } else { + return startYear; + } + } + + private int getEndYear() { + if (endYear == null) { + return LocalDate.now().getYear() + DEFAULT_YEARS_FROM_NOW; + } else { + return endYear; + } + } + @Override public void setLocale(Locale locale) { Locale oldLocale = getLocale(); if (locale != oldLocale || (locale != null && !locale.equals(oldLocale))) { - updateTimeZoneJSON(getZoneId(), locale); + updateTimeZoneJSON(getZoneId(), locale, getStartYear(), + getEndYear()); } super.setLocale(locale); } diff --git a/server/src/main/java/com/vaadin/util/TimeZoneUtil.java b/server/src/main/java/com/vaadin/util/TimeZoneUtil.java index 16e8323f9e..2997378de1 100644 --- a/server/src/main/java/com/vaadin/util/TimeZoneUtil.java +++ b/server/src/main/java/com/vaadin/util/TimeZoneUtil.java @@ -42,13 +42,14 @@ import elemental.json.impl.JsonUtil; public final class TimeZoneUtil implements Serializable { /** - * The start year used to send the time zone transition dates. + * The default start year (inclusive) from which to calculate the + * daylight-saving time zone transition dates. */ private static final int STARTING_YEAR = 1980; /** - * Till how many years from now, should we send the time zone transition - * dates. + * The default value of the number of future years from the current date for + * which the daylight-saving time zone transition dates are calculated. */ private static final int YEARS_FROM_NOW = 20; @@ -61,6 +62,12 @@ public final class TimeZoneUtil implements Serializable { * which is used in * {@link com.google.gwt.i18n.client.TimeZone#createTimeZone(String)}. * + * This method calculates the JSON string from the year + * {@value #STARTING_YEAR} until {@value #YEARS_FROM_NOW} years into the + * future from the current date. + * + * @see #toJSON(ZoneId, Locale, int, int) + * * @param zoneId * the {@link ZoneId} to get the daylight transitions from * @param locale @@ -69,6 +76,32 @@ public final class TimeZoneUtil implements Serializable { * @return the encoded string */ public static String toJSON(ZoneId zoneId, Locale locale) { + int endYear = LocalDate.now().getYear() + YEARS_FROM_NOW; + return toJSON(zoneId, locale, STARTING_YEAR, endYear); + } + + /** + * Returns a JSON string of the specified {@code zoneId} and {@link Locale}, + * which is used in + * {@link com.google.gwt.i18n.client.TimeZone#createTimeZone(String)}. + * + * This method calculates the JSON string from {@code startYear} until + * {@code startYear}, both inclusive. + * + * @param zoneId + * the {@link ZoneId} to get the daylight transitions from + * @param locale + * the locale used to determine the short name of the time zone + * @param startYear + * the start year of DST transitions + * @param endYear + * the end year of DST transitions + * + * @return the encoded string + * @since 8.11 + */ + public static String toJSON(ZoneId zoneId, Locale locale, int startYear, + int endYear) { if (zoneId == null || locale == null) { return null; } @@ -78,9 +111,8 @@ public final class TimeZoneUtil implements Serializable { TimeZoneInfo info = new TimeZoneInfo(); - int endYear = LocalDate.now().getYear() + YEARS_FROM_NOW; if (timeZone.useDaylightTime()) { - for (int year = STARTING_YEAR; year <= endYear; year++) { + for (int year = startYear; year <= endYear; year++) { ZonedDateTime i = LocalDateTime.of(year, 1, 1, 0, 0) .atZone(zoneId); while (true) { diff --git a/uitest/src/main/java/com/vaadin/tests/components/datefield/DateTimeFieldZoneIdFutureSummerDates.java b/uitest/src/main/java/com/vaadin/tests/components/datefield/DateTimeFieldZoneIdFutureSummerDates.java new file mode 100644 index 0000000000..f00d1d65fb --- /dev/null +++ b/uitest/src/main/java/com/vaadin/tests/components/datefield/DateTimeFieldZoneIdFutureSummerDates.java @@ -0,0 +1,185 @@ +package com.vaadin.tests.components.datefield; + +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.time.Month; +import java.time.ZoneId; +import java.util.Locale; +import java.util.Set; +import java.util.TreeSet; +import java.util.stream.Stream; + +import com.vaadin.server.UserError; +import com.vaadin.server.VaadinRequest; +import com.vaadin.tests.components.AbstractTestUI; +import com.vaadin.ui.ComboBox; +import com.vaadin.ui.DateField; +import com.vaadin.ui.DateTimeField; + +public class DateTimeFieldZoneIdFutureSummerDates extends AbstractTestUI { + + static final String ZONE_ID = "zoneId"; + static final String LOCALE_ID = "localeId"; + static final String START_YEAR_DATEFIELD_ID = "startYearDateFieldID"; + static final String END_YEAR_DATEFIELD_ID = "endYearDateFieldID"; + static final String FIXED_RANGE_DATEFIELD_ID = "fixedRangeDateFieldID"; + static final String VARIABLE_RANGE_DATEFIELD_ID = "variableRangeDateFieldID"; + + static final String INITIAL_ZONE_ID = "CET"; + static final Locale INITIAL_LOCALE = Locale.US; + static final LocalDateTime INITIAL_DATE_TIME = LocalDateTime + .of(LocalDate.now().getYear() + 21, Month.JULY, 1, 0, 0); + static final LocalDate INITIAL_START_DATE = LocalDate + .of(INITIAL_DATE_TIME.getYear() - 5, Month.JULY, 1); + static final LocalDate INITIAL_END_DATE = LocalDate + .of(INITIAL_DATE_TIME.getYear() + 5, Month.JULY, 1); + private static final String TARGET_FORMAT_PATTERN = "dd MMM yyyy - z"; + private static final String RANGE_FORMAT_PATTERN = "yyyy"; + + @Override + protected String getTestDescription() { + return "DateTimeField should correctly show the daylight saving (summer time) zone name " + + "of a date that occurs within a user-defined range"; + } + + @Override + protected Integer getTicketNumber() { + return 11919; + } + + @Override + protected void setup(VaadinRequest request) { + final ComboBox<String> zoneIdComboBox = getZoneIdComboBox(); + addComponent(zoneIdComboBox); + + final ComboBox<Locale> localeIdComboBox = getLocaleIdComboBox(); + addComponent(localeIdComboBox); + + final DateField transitionsStartYear = getDateField( + START_YEAR_DATEFIELD_ID, INITIAL_START_DATE, + RANGE_FORMAT_PATTERN, + "DST Transitions' start year (inclusive):"); + addComponent(transitionsStartYear); + + final DateField transitionsEndYear = getDateField(END_YEAR_DATEFIELD_ID, + INITIAL_END_DATE, RANGE_FORMAT_PATTERN, + "DST Transitions' end year (inclusive):"); + addComponent(transitionsEndYear); + + String captionVarField = "A DateTimeField with custom start" + + " and end years between which DST zone names are displayed:"; + final DateTimeField dateTimeFieldWithCustomRange = getDateTimeField( + VARIABLE_RANGE_DATEFIELD_ID, INITIAL_DATE_TIME, + TARGET_FORMAT_PATTERN, INITIAL_LOCALE, INITIAL_ZONE_ID, + captionVarField); + dateTimeFieldWithCustomRange.setDaylightSavingTimeRange( + INITIAL_START_DATE.getYear(), INITIAL_END_DATE.getYear()); + + addComponent(dateTimeFieldWithCustomRange); + + transitionsStartYear.addValueChangeListener(event -> { + int startYear = event.getValue().getYear(); + int endYear = transitionsEndYear.getValue().getYear(); + if (startYear > endYear) { + showDateRangeError(transitionsStartYear); + } else { + clearErrors(transitionsStartYear, transitionsEndYear); + dateTimeFieldWithCustomRange + .setDaylightSavingTimeRange(startYear, endYear); + } + }); + + transitionsEndYear.addValueChangeListener(event -> { + int startYear = transitionsStartYear.getValue().getYear(); + int endYear = event.getValue().getYear(); + if (startYear > endYear) { + showDateRangeError(transitionsEndYear); + } else { + clearErrors(transitionsStartYear, transitionsEndYear); + dateTimeFieldWithCustomRange + .setDaylightSavingTimeRange(startYear, endYear); + } + }); + + String captionFixedField = "A default DateTimeField (By default, " + + "DST zones are displayed between 1980 and 20 years into the future):"; + final DateTimeField dateTimeFieldWithDefaultRange = getDateTimeField( + FIXED_RANGE_DATEFIELD_ID, INITIAL_DATE_TIME, + TARGET_FORMAT_PATTERN, INITIAL_LOCALE, INITIAL_ZONE_ID, + captionFixedField); + addComponent(dateTimeFieldWithDefaultRange); + + zoneIdComboBox.addValueChangeListener(event -> { + final String value = event.getValue(); + if (value == null) { + dateTimeFieldWithCustomRange.setZoneId(null); + dateTimeFieldWithDefaultRange.setZoneId(null); + } else { + dateTimeFieldWithCustomRange.setZoneId(ZoneId.of(value)); + dateTimeFieldWithDefaultRange.setZoneId(ZoneId.of(value)); + } + }); + + localeIdComboBox.addValueChangeListener(event -> { + dateTimeFieldWithCustomRange.setLocale(event.getValue()); + dateTimeFieldWithDefaultRange.setLocale(event.getValue()); + }); + } + + private DateTimeField getDateTimeField(String id, + LocalDateTime initialDateTime, String dateFormat, Locale locale, + String zoneId, String caption) { + final DateTimeField dateTimeField = new DateTimeField(); + dateTimeField.setId(id); + dateTimeField.setValue(initialDateTime); + dateTimeField.setDateFormat(dateFormat); + dateTimeField.setLocale(locale); + dateTimeField.setZoneId(ZoneId.of(zoneId)); + dateTimeField.setCaption(caption); + return dateTimeField; + } + + private DateField getDateField(String id, LocalDate initialDate, + String dateFormat, String caption) { + final DateField dateField = new DateField(); + dateField.setId(id); + dateField.setDateFormat(dateFormat); + dateField.setCaption(caption); + dateField.setValue(initialDate); + return dateField; + } + + private void clearErrors(DateField transitionStartyear, + DateField transitionEndyear) { + transitionStartyear.setComponentError(null); + transitionEndyear.setComponentError(null); + } + + private void showDateRangeError(DateField dateField) { + dateField.setComponentError(new UserError( + "Start year must be less than or equal to end year!")); + } + + private ComboBox<Locale> getLocaleIdComboBox() { + final ComboBox<Locale> localeIdComboBox = new ComboBox<>(); + localeIdComboBox.setId(LOCALE_ID); + final Stream<Locale> localeStream = Stream + .of(Locale.getAvailableLocales()) + .sorted((l1, l2) -> l1.toString().compareTo(l2.toString())); + localeIdComboBox.setItems(localeStream); + localeIdComboBox.setValue(INITIAL_LOCALE); + localeIdComboBox.setCaption("Locale:"); + return localeIdComboBox; + } + + private ComboBox<String> getZoneIdComboBox() { + final ComboBox<String> zoneIdComboBox = new ComboBox<>(); + zoneIdComboBox.setId(ZONE_ID); + final Set<String> zoneIdSet = new TreeSet<>( + ZoneId.getAvailableZoneIds()); + zoneIdComboBox.setItems(zoneIdSet); + zoneIdComboBox.setValue(INITIAL_ZONE_ID); + zoneIdComboBox.setCaption("Zone:"); + return zoneIdComboBox; + } +} diff --git a/uitest/src/test/java/com/vaadin/tests/components/datefield/DateTimeFieldZoneIdFutureSummerDatesTest.java b/uitest/src/test/java/com/vaadin/tests/components/datefield/DateTimeFieldZoneIdFutureSummerDatesTest.java new file mode 100644 index 0000000000..1e7ea8eeef --- /dev/null +++ b/uitest/src/test/java/com/vaadin/tests/components/datefield/DateTimeFieldZoneIdFutureSummerDatesTest.java @@ -0,0 +1,155 @@ +package com.vaadin.tests.components.datefield; + +import static com.vaadin.tests.components.datefield.DateTimeFieldZoneIdFutureSummerDates.END_YEAR_DATEFIELD_ID; +import static com.vaadin.tests.components.datefield.DateTimeFieldZoneIdFutureSummerDates.FIXED_RANGE_DATEFIELD_ID; +import static com.vaadin.tests.components.datefield.DateTimeFieldZoneIdFutureSummerDates.LOCALE_ID; +import static com.vaadin.tests.components.datefield.DateTimeFieldZoneIdFutureSummerDates.START_YEAR_DATEFIELD_ID; +import static com.vaadin.tests.components.datefield.DateTimeFieldZoneIdFutureSummerDates.VARIABLE_RANGE_DATEFIELD_ID; +import static com.vaadin.tests.components.datefield.DateTimeFieldZoneIdFutureSummerDates.ZONE_ID; +import static org.junit.Assert.assertTrue; + +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.time.Month; +import java.util.Locale; + +import org.junit.Before; +import org.junit.Test; + +import com.vaadin.testbench.elements.ComboBoxElement; +import com.vaadin.testbench.elements.DateFieldElement; +import com.vaadin.testbench.elements.DateTimeFieldElement; +import com.vaadin.tests.tb3.SingleBrowserTest; + +public class DateTimeFieldZoneIdFutureSummerDatesTest + extends SingleBrowserTest { + + private static final String TESTING_ZONE_ID = "CET"; + private static final String TESTING_LOCALE = Locale.US.toString(); + + DateTimeFieldElement dateTimeFieldWithVariableRange; + DateTimeFieldElement dateTimeFieldWithDefaultRange; + + DateFieldElement transitionsStartYear; + DateFieldElement transitionsEndYear; + + ComboBoxElement zoneIdComboBox; + ComboBoxElement localeIdComboBox; + + @Before + public void init() { + openTestURL(); + + dateTimeFieldWithVariableRange = $(DateTimeFieldElement.class) + .id(VARIABLE_RANGE_DATEFIELD_ID); + dateTimeFieldWithDefaultRange = $(DateTimeFieldElement.class) + .id(FIXED_RANGE_DATEFIELD_ID); + + transitionsStartYear = $(DateFieldElement.class) + .id(START_YEAR_DATEFIELD_ID); + transitionsEndYear = $(DateFieldElement.class).id(END_YEAR_DATEFIELD_ID); + + zoneIdComboBox = $(ComboBoxElement.class).id(ZONE_ID); + zoneIdComboBox.selectByText(TESTING_ZONE_ID); + localeIdComboBox = $(ComboBoxElement.class).id(LOCALE_ID); + localeIdComboBox.selectByText(TESTING_LOCALE); + } + + @Test + public void dateTimeFieldWithCustomRangeShouldShowDSTWithinRange() { + final int testingRangeCentralYear = LocalDate.now().getYear() + 50; + final int testingRangeUpperYear = testingRangeCentralYear + 3; + final int testingRangeLowerYear = testingRangeCentralYear - 3; + + transitionsEndYear.setDate(LocalDate.of(testingRangeUpperYear, 1, 1)); + transitionsStartYear.setDate(LocalDate.of(testingRangeLowerYear, 1, 1)); + + LocalDateTime testingDateTime = LocalDateTime + .of(testingRangeCentralYear, Month.JULY, 1, 0, 0); + dateTimeFieldWithVariableRange.setDateTime(testingDateTime); + assertEndsWith(dateTimeFieldWithVariableRange, "CEST"); + + testingDateTime = LocalDateTime.of(testingRangeUpperYear, Month.JULY, 1, + 0, 0); + dateTimeFieldWithVariableRange.setDateTime(testingDateTime); + assertEndsWith(dateTimeFieldWithVariableRange, "CEST"); + + testingDateTime = LocalDateTime.of(testingRangeLowerYear, Month.JULY, 1, + 0, 0); + dateTimeFieldWithVariableRange.setDateTime(testingDateTime); + assertEndsWith(dateTimeFieldWithVariableRange, "CEST"); + } + + @Test + public void dateTimeFieldWithCustomRangeShouldNotShowDSTOutsideRange() { + final int testingRangeCentralYear = LocalDate.now().getYear() + 50; + final int testingRangeUpperYear = testingRangeCentralYear + 3; + final int testingRangeLowerYear = testingRangeCentralYear - 3; + + transitionsEndYear.setDate(LocalDate.of(testingRangeUpperYear, 1, 1)); + transitionsStartYear.setDate(LocalDate.of(testingRangeLowerYear, 1, 1)); + + // This year is out of specified range + LocalDateTime testingDateTime = LocalDateTime + .of(LocalDate.now().getYear(), Month.JULY, 1, 0, 0); + dateTimeFieldWithVariableRange.setDateTime(testingDateTime); + assertEndsWith(dateTimeFieldWithVariableRange, "CET"); + + // One year after the specified range + testingDateTime = LocalDateTime.of(testingRangeUpperYear + 1, + Month.JULY, 1, 0, 0); + dateTimeFieldWithVariableRange.setDateTime(testingDateTime); + assertEndsWith(dateTimeFieldWithVariableRange, "CET"); + + // One year before the specified range + testingDateTime = LocalDateTime.of(testingRangeLowerYear - 1, + Month.JULY, 1, 0, 0); + dateTimeFieldWithVariableRange.setDateTime(testingDateTime); + assertEndsWith(dateTimeFieldWithVariableRange, "CET"); + } + + @Test + public void dateTimeFieldWithDefaultRangeShouldShowDSTFrom1980Until20FutureYears() { + // The 1980 to 20 future years range is the hard-coded default range + // for which DST is shown if user doesn't provide a custom range + + final int testingRangeLowerYear = 1980; + LocalDateTime testingDateTime = LocalDateTime.of(testingRangeLowerYear, + Month.JULY, 1, 0, 0); + dateTimeFieldWithDefaultRange.setDateTime(testingDateTime); + assertEndsWith(dateTimeFieldWithDefaultRange, "CEST"); + + final int testingRangeUpperYear = LocalDate.now().getYear() + 20; + testingDateTime = LocalDateTime.of(testingRangeUpperYear, Month.JULY, 1, + 0, 0); + dateTimeFieldWithDefaultRange.setDateTime(testingDateTime); + assertEndsWith(dateTimeFieldWithDefaultRange, "CEST"); + + final int testingCurrYear = LocalDate.now().getYear(); + testingDateTime = LocalDateTime.of(testingCurrYear, Month.JULY, 1, 0, + 0); + dateTimeFieldWithDefaultRange.setDateTime(testingDateTime); + assertEndsWith(dateTimeFieldWithDefaultRange, "CEST"); + } + + @Test + public void dateTimeFieldWithDefaultRangeShouldNotShowDSTBefore1980() { + final LocalDateTime testingDateTime = LocalDateTime.of(1979, Month.JULY, + 1, 0, 0); + dateTimeFieldWithDefaultRange.setDateTime(testingDateTime); + assertEndsWith(dateTimeFieldWithDefaultRange, "CET"); + } + + @Test + public void dateTimeFieldWithDefaultRangeShouldNotShowDSTAfter20FutureYears() { + final LocalDateTime testingDateTime = LocalDateTime + .of(LocalDate.now().getYear() + 21, Month.JULY, 1, 0, 0); + dateTimeFieldWithDefaultRange.setDateTime(testingDateTime); + assertEndsWith(dateTimeFieldWithDefaultRange, "CET"); + } + + private void assertEndsWith(DateTimeFieldElement element, String suffix) { + final String text = element.getValue(); + assertTrue(text + " should end with " + suffix, text.endsWith(suffix)); + } +} |