Browse Source

DateField value should actively adjust to the set resolution. (#12183)

tags/7.7.23
Anna Koskinen 3 years ago
parent
commit
b4f011230f
No account linked to committer's email address

+ 41
- 21
server/src/main/java/com/vaadin/ui/AbstractDateField.java View File

@@ -38,9 +38,9 @@ import java.util.logging.Logger;
import java.util.stream.Collectors;
import java.util.stream.Stream;

import com.googlecode.gentyref.GenericTypeReflector;
import org.jsoup.nodes.Element;

import com.googlecode.gentyref.GenericTypeReflector;
import com.vaadin.data.Result;
import com.vaadin.data.ValidationResult;
import com.vaadin.data.Validator;
@@ -179,7 +179,7 @@ public abstract class AbstractDateField<T extends Temporal & TemporalAdjuster &
};

/**
* The default start year (inclusive) from which to calculate the
* The default start year (inclusive) from which to calculate the
* daylight-saving time zone transition dates.
*/
private static final int DEFAULT_START_YEAR = 1980;
@@ -323,8 +323,9 @@ public abstract class AbstractDateField<T extends Temporal & TemporalAdjuster &
* <p>
* Note: Negative, i.e. BC dates are not supported.
* <p>
* Note: It's usually recommended to use only one of the following at the same
* time: Range validator with Binder or DateField's setRangeStart check.
* Note: It's usually recommended to use only one of the following at the
* same time: Range validator with Binder or DateField's setRangeStart
* check.
*
* @param startDate
* - the allowed range's start date
@@ -377,8 +378,11 @@ public abstract class AbstractDateField<T extends Temporal & TemporalAdjuster &
* the resolution to set, not {@code null}
*/
public void setResolution(R resolution) {
this.resolution = resolution;
updateResolutions();
if (!resolution.equals(this.resolution)) {
this.resolution = resolution;
setValue(adjustToResolution(getValue(), resolution));
updateResolutions();
}
}

/**
@@ -387,8 +391,8 @@ public abstract class AbstractDateField<T extends Temporal & TemporalAdjuster &
* validate. If {@code endDate} is set to {@code null}, any value after
* {@code startDate} will be accepted by the range.
* <p>
* Note: It's usually recommended to use only one of the following at the same
* time: Range validator with Binder or DateField's setRangeEnd check.
* Note: It's usually recommended to use only one of the following at the
* same time: Range validator with Binder or DateField's setRangeEnd check.
*
* @param endDate
* the allowed range's end date (inclusive, based on the current
@@ -545,8 +549,8 @@ public abstract class AbstractDateField<T extends Temporal & TemporalAdjuster &
* 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.
* 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
@@ -704,12 +708,13 @@ public abstract class AbstractDateField<T extends Temporal & TemporalAdjuster &
* @param value
* the new value, may be {@code null}
* @throws IllegalArgumentException
* if the value is not within range bounds
* if the value is not within range bounds
*/
@Override
public void setValue(T value) {
T adjusted = adjustToResolution(value, getResolution());
RangeValidator<T> validator = getRangeValidator();
ValidationResult result = validator.apply(value,
ValidationResult result = validator.apply(adjusted,
new ValueContext(this, this));

if (result.isError()) {
@@ -718,25 +723,40 @@ public abstract class AbstractDateField<T extends Temporal & TemporalAdjuster &
} else {
currentErrorMessage = null;
/*
* First handle special case when the client side component has a date
* string but value is null (e.g. unparsable date string typed in by the
* user). No value changes should happen, but we need to do some
* internal housekeeping.
* First handle special case when the client side component has a
* date string but value is null (e.g. unparsable date string typed
* in by the user). No value changes should happen, but we need to
* do some internal housekeeping.
*/
if (value == null && !getState(false).parsable) {
if (adjusted == null && !getState(false).parsable) {
/*
* Side-effects of doSetValue clears possible previous strings and
* flags about invalid input.
* Side-effects of doSetValue clears possible previous strings
* and flags about invalid input.
*/
doSetValue(null);

markAsDirty();
return;
}
super.setValue(value);
super.setValue(adjusted);
}
}

/**
* Adjusts the given date to the given resolution. Any values that are more
* specific than the given resolution are truncated to their default values.
*
* @param date
* the date to adjust, can be {@code null}
* @param resolution
* the resolution to be used in the adjustment, can be
* {@code null}
* @return an adjusted date that matches the given resolution, or
* {@code null} if the given date, resolution, or both were
* {@code null}
*/
protected abstract T adjustToResolution(T date, R resolution);

/**
* Checks whether ISO 8601 week numbers are shown in the date selector.
*
@@ -820,7 +840,7 @@ public abstract class AbstractDateField<T extends Temporal & TemporalAdjuster &
.info("cannot parse " + design.attr("value")
+ " as date");
}
doSetValue(date);
doSetValue(adjustToResolution(date, getResolution()));
} else {
throw new RuntimeException("Cannot detect resoluton type "
+ Optional.ofNullable(dateType).map(Type::getTypeName)

+ 12
- 8
server/src/main/java/com/vaadin/ui/AbstractLocalDateField.java View File

@@ -103,8 +103,8 @@ public abstract class AbstractLocalDateField
@Override
protected RangeValidator<LocalDate> getRangeValidator() {
return new DateRangeValidator(getDateOutOfRangeMessage(),
getDate(getRangeStart(), getResolution()),
getDate(getRangeEnd(), getResolution()));
adjustToResolution(getRangeStart(), getResolution()),
adjustToResolution(getRangeEnd(), getResolution()));
}

@Override
@@ -134,7 +134,9 @@ public abstract class AbstractLocalDateField
return Date.from(date.atStartOfDay(ZoneOffset.UTC).toInstant());
}

private LocalDate getDate(LocalDate date, DateResolution forResolution) {
@Override
protected LocalDate adjustToResolution(LocalDate date,
DateResolution forResolution) {
if (date == null) {
return null;
}
@@ -171,19 +173,21 @@ public abstract class AbstractLocalDateField
protected Result<LocalDate> handleUnparsableDateString(String dateString) {
// Handle possible week number, which cannot be parsed client side due
// limitations in GWT
if (this.getDateFormat() != null && this.getDateFormat().contains("w")) {
if (getDateFormat() != null && getDateFormat().contains("w")) {
Date parsedDate;
SimpleDateFormat df = new SimpleDateFormat(this.getDateFormat(),this.getLocale());
SimpleDateFormat df = new SimpleDateFormat(getDateFormat(),
getLocale());
try {
parsedDate = df.parse(dateString);
} catch (ParseException e) {
return super.handleUnparsableDateString(dateString);
}
ZoneId zi = this.getZoneId();
if (zi == null) {
ZoneId zi = getZoneId();
if (zi == null) {
zi = ZoneId.systemDefault();
}
LocalDate date = Instant.ofEpochMilli(parsedDate.getTime()).atZone(zi).toLocalDate();
LocalDate date = Instant.ofEpochMilli(parsedDate.getTime())
.atZone(zi).toLocalDate();
return Result.ok(date);
} else {
return super.handleUnparsableDateString(dateString);

+ 4
- 3
server/src/main/java/com/vaadin/ui/AbstractLocalDateTimeField.java View File

@@ -111,8 +111,8 @@ public abstract class AbstractLocalDateTimeField
@Override
protected RangeValidator<LocalDateTime> getRangeValidator() {
return new DateTimeRangeValidator(getDateOutOfRangeMessage(),
getDate(getRangeStart(), getResolution()),
getDate(getRangeEnd(), getResolution()));
adjustToResolution(getRangeStart(), getResolution()),
adjustToResolution(getRangeEnd(), getResolution()));
}

@Override
@@ -143,7 +143,8 @@ public abstract class AbstractLocalDateTimeField
return Date.from(date.toInstant(ZoneOffset.UTC));
}

private LocalDateTime getDate(LocalDateTime date,
@Override
protected LocalDateTime adjustToResolution(LocalDateTime date,
DateTimeResolution forResolution) {
if (date == null) {
return null;

+ 10
- 1
server/src/test/java/com/vaadin/tests/data/converter/LocalDateTimeToDateConverterTest.java View File

@@ -14,6 +14,7 @@ import com.vaadin.data.Binder;
import com.vaadin.data.ValidationException;
import com.vaadin.data.ValueContext;
import com.vaadin.data.converter.LocalDateTimeToDateConverter;
import com.vaadin.shared.ui.datefield.DateTimeResolution;
import com.vaadin.ui.DateTimeField;

public class LocalDateTimeToDateConverterTest extends AbstractConverterTest {
@@ -59,7 +60,15 @@ public class LocalDateTimeToDateConverterTest extends AbstractConverterTest {
BeanWithDate bean = new BeanWithDate();
binder.writeBean(bean);

assertEquals(DATE, bean.getDate());
assertEquals(DateTimeResolution.MINUTE, dateField.getResolution());

// create a comparison date that matches the resolution
Calendar calendar = Calendar
.getInstance(TimeZone.getTimeZone(ZoneOffset.UTC));
calendar.clear();
calendar.set(2017, Calendar.JANUARY, 1, 1, 1, 0);
Date date = calendar.getTime();
assertEquals(date, bean.getDate());
}

public static class BeanWithDate {

+ 10
- 6
server/src/test/java/com/vaadin/tests/server/component/abstractdatefield/AbstractLocalDateTimeFieldDeclarativeTest.java View File

@@ -25,15 +25,19 @@ import com.vaadin.ui.AbstractLocalDateTimeField;
public abstract class AbstractLocalDateTimeFieldDeclarativeTest<T extends AbstractLocalDateTimeField>
extends AbstractFieldDeclarativeTest<T, LocalDateTime> {

protected DateTimeFormatter DATE_FORMATTER = DateTimeFormatter
.ofPattern("yyyy-MM-dd HH:mm:ss", Locale.ENGLISH);
// field initialised with DateTimeResolution.MINUTE, seconds get truncated
protected DateTimeFormatter VALUE_DATE_FORMATTER = DateTimeFormatter
.ofPattern("yyyy-MM-dd HH:mm:00", Locale.ROOT);
// only field value conforms to resolution, range keeps the initial values
protected DateTimeFormatter RANGE_DATE_FORMATTER = DateTimeFormatter
.ofPattern("yyyy-MM-dd HH:mm:ss", Locale.ROOT);

@Override
public void valueDeserialization()
throws InstantiationException, IllegalAccessException {
LocalDateTime value = LocalDateTime.of(2003, 02, 27, 10, 37, 43);
String design = String.format("<%s value='%s'/>", getComponentTag(),
DATE_FORMATTER.format(value));
VALUE_DATE_FORMATTER.format(value));

T component = getComponentClass().newInstance();
component.setValue(value);
@@ -57,8 +61,8 @@ public abstract class AbstractLocalDateTimeFieldDeclarativeTest<T extends Abstra
"<%s show-iso-week-numbers range-end='%s' range-start='%s' "
+ "date-out-of-range-message='%s' resolution='%s' "
+ "date-format='%s' lenient parse-error-message='%s'/>",
getComponentTag(), DATE_FORMATTER.format(end),
DATE_FORMATTER.format(start), dateOutOfRange,
getComponentTag(), RANGE_DATE_FORMATTER.format(end),
RANGE_DATE_FORMATTER.format(start), dateOutOfRange,
resolution.name().toLowerCase(Locale.ROOT), dateFormat,
parseErrorMsg);

@@ -82,7 +86,7 @@ public abstract class AbstractLocalDateTimeFieldDeclarativeTest<T extends Abstra
throws InstantiationException, IllegalAccessException {
LocalDateTime value = LocalDateTime.of(2003, 02, 27, 23, 12, 34);
String design = String.format("<%s value='%s' readonly/>",
getComponentTag(), DATE_FORMATTER.format(value));
getComponentTag(), VALUE_DATE_FORMATTER.format(value));

T component = getComponentClass().newInstance();
component.setValue(value);

+ 31
- 1
server/src/test/java/com/vaadin/tests/server/component/datefield/DateFieldListenersTest.java View File

@@ -1,12 +1,14 @@
package com.vaadin.tests.server.component.datefield;

import java.time.LocalDateTime;
import java.time.temporal.ChronoUnit;
import java.time.temporal.TemporalAccessor;
import java.util.Date;
import java.util.Map;

import org.junit.Test;

import com.vaadin.data.validator.DateTimeRangeValidator;
import com.vaadin.data.validator.RangeValidator;
import com.vaadin.event.FieldEvents.BlurEvent;
import com.vaadin.event.FieldEvents.BlurListener;
@@ -39,7 +41,9 @@ public class DateFieldListenersTest extends AbstractListenerMethodsTestBase {

@Override
protected RangeValidator<LocalDateTime> getRangeValidator() {
return null;
return new DateTimeRangeValidator(getDateOutOfRangeMessage(),
adjustToResolution(getRangeStart(), getResolution()),
adjustToResolution(getRangeEnd(), getResolution()));
}

@Override
@@ -61,6 +65,32 @@ public class DateFieldListenersTest extends AbstractListenerMethodsTestBase {
protected LocalDateTime toType(TemporalAccessor temporalAccessor) {
return LocalDateTime.from(temporalAccessor);
}

@Override
protected LocalDateTime adjustToResolution(LocalDateTime date,
DateTimeResolution forResolution) {
if (date == null) {
return null;
}
switch (forResolution) {
case YEAR:
return date.withDayOfYear(1).toLocalDate().atStartOfDay();
case MONTH:
return date.withDayOfMonth(1).toLocalDate().atStartOfDay();
case DAY:
return date.toLocalDate().atStartOfDay();
case HOUR:
return date.truncatedTo(ChronoUnit.HOURS);
case MINUTE:
return date.truncatedTo(ChronoUnit.MINUTES);
case SECOND:
return date.truncatedTo(ChronoUnit.SECONDS);
default:
assert false : "Unexpected resolution argument "
+ forResolution;
return null;
}
}
}

@Test

+ 114
- 0
uitest/src/main/java/com/vaadin/tests/components/datefield/DateFieldResolutionChange.java View File

@@ -0,0 +1,114 @@
package com.vaadin.tests.components.datefield;

import java.time.LocalDate;
import java.time.format.DateTimeFormatter;
import java.util.Locale;

import com.vaadin.data.Binder;
import com.vaadin.server.VaadinRequest;
import com.vaadin.shared.ui.datefield.DateResolution;
import com.vaadin.tests.components.AbstractTestUIWithLog;
import com.vaadin.ui.Button;
import com.vaadin.ui.DateField;
import com.vaadin.ui.HorizontalLayout;

public class DateFieldResolutionChange extends AbstractTestUIWithLog {

protected DateTimeFormatter DATE_FORMATTER = DateTimeFormatter
.ofPattern("yyyy-MM-dd", Locale.ROOT);

@Override
protected void setup(VaadinRequest request) {
Binder<Pojo> binder = new Binder<>(Pojo.class);

HorizontalLayout horizontalLayout = new HorizontalLayout();

final DateField monthField = new DateField() {
@Override
public void setValue(LocalDate value) {
if (value != null) {
log("MonthField set value " + DATE_FORMATTER.format(value));
}
super.setValue(value);
}
};
monthField.setResolution(DateResolution.MONTH);
monthField.setId("MonthField");
monthField.addValueChangeListener(
event -> log("MonthField value change event: "
+ DATE_FORMATTER.format(event.getValue())));
binder.bind(monthField, "value1");

final DateField dayField = new DateField() {
@Override
public void setValue(LocalDate value) {
if (value != null) {
log("DayField set value " + DATE_FORMATTER.format(value));
}
super.setValue(value);
}
};
dayField.setResolution(DateResolution.DAY);
dayField.setId("DayField");
dayField.addValueChangeListener(
event -> log("DayField value change event: "
+ DATE_FORMATTER.format(event.getValue())));
binder.bind(dayField, "value2");

Pojo pojo = new Pojo();
binder.setBean(pojo);

Button monthButton = new Button("month", e -> {
monthField.setResolution(DateResolution.MONTH);
dayField.setResolution(DateResolution.MONTH);
});

Button dayButton = new Button("day", e -> {
monthField.setResolution(DateResolution.DAY);
dayField.setResolution(DateResolution.DAY);
});

Button logButton = new Button("log", e -> {
log("MonthField current value: "
+ DATE_FORMATTER.format(pojo.getValue1()));
log("DayField current value: "
+ DATE_FORMATTER.format(pojo.getValue2()));
});

Button setButton = new Button("set", e -> {
LocalDate newDate = LocalDate.of(2021, 2, 14);
pojo.setValue1(newDate);
pojo.setValue2(newDate);
binder.setBean(pojo);
});

horizontalLayout.addComponents(monthField, dayField, monthButton,
dayButton, logButton, setButton);
addComponent(horizontalLayout);
}

public class Pojo {
private LocalDate value1, value2 = null;

public LocalDate getValue1() {
return value1;
}

public void setValue1(LocalDate value1) {
this.value1 = value1;
}

public LocalDate getValue2() {
return value2;
}

public void setValue2(LocalDate value2) {
this.value2 = value2;
}
}

@Override
protected String getTestDescription() {
return "Date field value should immediately update to match resolution.";
}
}

+ 114
- 0
uitest/src/main/java/com/vaadin/tests/components/datefield/DateTimeFieldResolutionChange.java View File

@@ -0,0 +1,114 @@
package com.vaadin.tests.components.datefield;

import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.Locale;

import com.vaadin.data.Binder;
import com.vaadin.server.VaadinRequest;
import com.vaadin.shared.ui.datefield.DateTimeResolution;
import com.vaadin.tests.components.AbstractTestUIWithLog;
import com.vaadin.ui.Button;
import com.vaadin.ui.DateTimeField;
import com.vaadin.ui.HorizontalLayout;

public class DateTimeFieldResolutionChange extends AbstractTestUIWithLog {

protected DateTimeFormatter DATE_FORMATTER = DateTimeFormatter
.ofPattern("yyyy-MM-dd HH:mm:ss", Locale.ROOT);

@Override
protected void setup(VaadinRequest request) {
Binder<Pojo> binder = new Binder<>(Pojo.class);

HorizontalLayout horizontalLayout = new HorizontalLayout();

final DateTimeField monthField = new DateTimeField() {
@Override
public void setValue(LocalDateTime value) {
if (value != null) {
log("MonthField set value " + DATE_FORMATTER.format(value));
}
super.setValue(value);
}
};
monthField.setResolution(DateTimeResolution.MONTH);
monthField.setId("MonthField");
monthField.addValueChangeListener(
event -> log("MonthField value change event: "
+ DATE_FORMATTER.format(event.getValue())));
binder.bind(monthField, "value1");

final DateTimeField dayField = new DateTimeField() {
@Override
public void setValue(LocalDateTime value) {
if (value != null) {
log("DayField set value " + DATE_FORMATTER.format(value));
}
super.setValue(value);
}
};
dayField.setResolution(DateTimeResolution.DAY);
dayField.setId("DayField");
dayField.addValueChangeListener(
event -> log("DayField value change event: "
+ DATE_FORMATTER.format(event.getValue())));
binder.bind(dayField, "value2");

Pojo pojo = new Pojo();
binder.setBean(pojo);

Button monthButton = new Button("month", e -> {
monthField.setResolution(DateTimeResolution.MONTH);
dayField.setResolution(DateTimeResolution.MONTH);
});

Button dayButton = new Button("day", e -> {
monthField.setResolution(DateTimeResolution.DAY);
dayField.setResolution(DateTimeResolution.DAY);
});

Button logButton = new Button("log", e -> {
log("MonthField current value: "
+ DATE_FORMATTER.format(pojo.getValue1()));
log("DayField current value: "
+ DATE_FORMATTER.format(pojo.getValue2()));
});

Button setButton = new Button("set", e -> {
LocalDateTime newDate = LocalDateTime.of(2021, 2, 14, 16, 17);
pojo.setValue1(newDate);
pojo.setValue2(newDate);
binder.setBean(pojo);
});

horizontalLayout.addComponents(monthField, dayField, monthButton,
dayButton, logButton, setButton);
addComponent(horizontalLayout);
}

public class Pojo {
private LocalDateTime value1, value2 = null;

public LocalDateTime getValue1() {
return value1;
}

public void setValue1(LocalDateTime value1) {
this.value1 = value1;
}

public LocalDateTime getValue2() {
return value2;
}

public void setValue2(LocalDateTime value2) {
this.value2 = value2;
}
}

@Override
protected String getTestDescription() {
return "Date field value should immediately update to match resolution.";
}
}

+ 50
- 0
uitest/src/test/java/com/vaadin/tests/components/datefield/DateFieldResolutionChangeTest.java View File

@@ -0,0 +1,50 @@
package com.vaadin.tests.components.datefield;

import static org.junit.Assert.assertEquals;

import org.junit.Test;

import com.vaadin.testbench.elements.ButtonElement;
import com.vaadin.tests.tb3.MultiBrowserTest;

public class DateFieldResolutionChangeTest extends MultiBrowserTest {

@Test
public void testValueAndResolutionChange() {
openTestURL();

// set a fixed date
$(ButtonElement.class).caption("set").first().click();

// both fields should trigger a value change event but for MonthField
// the day value should be truncated to default
assertEquals("Unexpected log row", "1. MonthField set value 2021-02-14",
getLogRow(3));
assertEquals("Unexpected log row",
"2. MonthField value change event: 2021-02-01", getLogRow(2));
assertEquals("Unexpected log row", "3. DayField set value 2021-02-14",
getLogRow(1));
assertEquals("Unexpected log row",
"4. DayField value change event: 2021-02-14", getLogRow(0));

// change both to day resolution
$(ButtonElement.class).caption("day").first().click();

// DayField shouldn't react, MonthField should check that the value
// matches resolution but not trigger a ValueChangeEvent
assertEquals("Unexpected log row", "5. MonthField set value 2021-02-01",
getLogRow(0));

// change both to month resolution
$(ButtonElement.class).caption("month").first().click();

// both fields should check that the value matches resolution but only
// DayField should trigger a ValueChangeEvent
assertEquals("Unexpected log row", "6. MonthField set value 2021-02-01",
getLogRow(2));
assertEquals("Unexpected log row", "7. DayField set value 2021-02-01",
getLogRow(1));
assertEquals("Unexpected log row",
"8. DayField value change event: 2021-02-01", getLogRow(0));
}
}

+ 53
- 0
uitest/src/test/java/com/vaadin/tests/components/datefield/DateTimeFieldResolutionChangeTest.java View File

@@ -0,0 +1,53 @@
package com.vaadin.tests.components.datefield;

import static org.junit.Assert.assertEquals;

import org.junit.Test;

import com.vaadin.testbench.elements.ButtonElement;
import com.vaadin.tests.tb3.MultiBrowserTest;

public class DateTimeFieldResolutionChangeTest extends MultiBrowserTest {

@Test
public void testValueAndResolutionChange() {
openTestURL();

// set a fixed date
$(ButtonElement.class).caption("set").first().click();

// both fields should trigger a value change event but the value should
// be truncated according to each field's resolution
assertEquals("Unexpected log row",
"1. MonthField set value 2021-02-14 16:17:00", getLogRow(3));
assertEquals("Unexpected log row",
"2. MonthField value change event: 2021-02-01 00:00:00",
getLogRow(2));
assertEquals("Unexpected log row",
"3. DayField set value 2021-02-14 16:17:00", getLogRow(1));
assertEquals("Unexpected log row",
"4. DayField value change event: 2021-02-14 00:00:00",
getLogRow(0));

// change both to day resolution
$(ButtonElement.class).caption("day").first().click();

// DayField shouldn't react, MonthField should check that the value
// matches resolution but not trigger a ValueChangeEvent
assertEquals("Unexpected log row",
"5. MonthField set value 2021-02-01 00:00:00", getLogRow(0));

// change both to month resolution
$(ButtonElement.class).caption("month").first().click();

// both fields should check that the value matches resolution but only
// DayField should trigger a ValueChangeEvent
assertEquals("Unexpected log row",
"6. MonthField set value 2021-02-01 00:00:00", getLogRow(2));
assertEquals("Unexpected log row",
"7. DayField set value 2021-02-01 00:00:00", getLogRow(1));
assertEquals("Unexpected log row",
"8. DayField value change event: 2021-02-01 00:00:00",
getLogRow(0));
}
}

Loading…
Cancel
Save