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;
};
/**
- * 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;
* <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
* 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();
+ }
}
/**
* 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
* 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
* @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()) {
} 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.
*
.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)
@Override
protected RangeValidator<LocalDate> getRangeValidator() {
return new DateRangeValidator(getDateOutOfRangeMessage(),
- getDate(getRangeStart(), getResolution()),
- getDate(getRangeEnd(), getResolution()));
+ adjustToResolution(getRangeStart(), getResolution()),
+ adjustToResolution(getRangeEnd(), getResolution()));
}
@Override
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;
}
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);
@Override
protected RangeValidator<LocalDateTime> getRangeValidator() {
return new DateTimeRangeValidator(getDateOutOfRangeMessage(),
- getDate(getRangeStart(), getResolution()),
- getDate(getRangeEnd(), getResolution()));
+ adjustToResolution(getRangeStart(), getResolution()),
+ adjustToResolution(getRangeEnd(), getResolution()));
}
@Override
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;
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 {
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 {
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);
"<%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);
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);
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;
@Override
protected RangeValidator<LocalDateTime> getRangeValidator() {
- return null;
+ return new DateTimeRangeValidator(getDateOutOfRangeMessage(),
+ adjustToResolution(getRangeStart(), getResolution()),
+ adjustToResolution(getRangeEnd(), getResolution()));
}
@Override
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
--- /dev/null
+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.";
+ }
+}
--- /dev/null
+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.";
+ }
+}
--- /dev/null
+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));
+ }
+}
--- /dev/null
+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));
+ }
+}