You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

AbstractDateField.java 32KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984
  1. /*
  2. * Copyright 2000-2018 Vaadin Ltd.
  3. *
  4. * Licensed under the Apache License, Version 2.0 (the "License"); you may not
  5. * use this file except in compliance with the License. You may obtain a copy of
  6. * the License at
  7. *
  8. * http://www.apache.org/licenses/LICENSE-2.0
  9. *
  10. * Unless required by applicable law or agreed to in writing, software
  11. * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
  12. * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
  13. * License for the specific language governing permissions and limitations under
  14. * the License.
  15. */
  16. package com.vaadin.ui;
  17. import java.io.Serializable;
  18. import java.lang.reflect.Type;
  19. import java.text.SimpleDateFormat;
  20. import java.time.LocalDate;
  21. import java.time.ZoneId;
  22. import java.time.format.DateTimeFormatter;
  23. import java.time.temporal.Temporal;
  24. import java.time.temporal.TemporalAccessor;
  25. import java.time.temporal.TemporalAdjuster;
  26. import java.util.Calendar;
  27. import java.util.Collections;
  28. import java.util.Date;
  29. import java.util.HashMap;
  30. import java.util.Locale;
  31. import java.util.Map;
  32. import java.util.Map.Entry;
  33. import java.util.Objects;
  34. import java.util.Optional;
  35. import java.util.Set;
  36. import java.util.logging.Logger;
  37. import java.util.stream.Collectors;
  38. import java.util.stream.Stream;
  39. import elemental.json.Json;
  40. import org.jsoup.nodes.Element;
  41. import com.googlecode.gentyref.GenericTypeReflector;
  42. import com.vaadin.data.Result;
  43. import com.vaadin.data.ValidationResult;
  44. import com.vaadin.data.Validator;
  45. import com.vaadin.data.ValueContext;
  46. import com.vaadin.data.validator.RangeValidator;
  47. import com.vaadin.event.FieldEvents.BlurEvent;
  48. import com.vaadin.event.FieldEvents.BlurListener;
  49. import com.vaadin.event.FieldEvents.BlurNotifier;
  50. import com.vaadin.event.FieldEvents.FocusEvent;
  51. import com.vaadin.event.FieldEvents.FocusListener;
  52. import com.vaadin.event.FieldEvents.FocusNotifier;
  53. import com.vaadin.server.ErrorMessage;
  54. import com.vaadin.server.UserError;
  55. import com.vaadin.shared.Registration;
  56. import com.vaadin.shared.ui.datefield.AbstractDateFieldServerRpc;
  57. import com.vaadin.shared.ui.datefield.AbstractDateFieldState;
  58. import com.vaadin.shared.ui.datefield.AbstractDateFieldState.AccessibleElement;
  59. import com.vaadin.shared.ui.datefield.DateResolution;
  60. import com.vaadin.ui.declarative.DesignAttributeHandler;
  61. import com.vaadin.ui.declarative.DesignContext;
  62. import com.vaadin.util.TimeZoneUtil;
  63. /**
  64. * A date editor component with {@link LocalDate} as an input value.
  65. *
  66. * @author Vaadin Ltd
  67. *
  68. * @since 8.0
  69. *
  70. * @param <T>
  71. * type of date ({@code LocalDate} or {@code LocalDateTime}).
  72. * @param <R>
  73. * resolution enumeration type
  74. *
  75. */
  76. public abstract class AbstractDateField<T extends Temporal & TemporalAdjuster & Serializable & Comparable<? super T>, R extends Enum<R>>
  77. extends AbstractField<T> implements FocusNotifier, BlurNotifier {
  78. private static final DateTimeFormatter RANGE_FORMATTER = DateTimeFormatter
  79. .ofPattern("yyyy-MM-dd[ HH:mm:ss]", Locale.ENGLISH);
  80. private AbstractDateFieldServerRpc rpc = new AbstractDateFieldServerRpc() {
  81. @Override
  82. public void update(String newDateString,
  83. Map<String, Integer> resolutions) {
  84. Set<String> resolutionNames = getResolutions().map(Enum::name)
  85. .collect(Collectors.toSet());
  86. resolutionNames.retainAll(resolutions.keySet());
  87. if (!isReadOnly()
  88. && (!resolutionNames.isEmpty() || newDateString != null)) {
  89. // Old and new dates
  90. final T oldDate = getValue();
  91. T newDate;
  92. boolean hasChanges = false;
  93. if ("".equals(newDateString)) {
  94. newDate = null;
  95. } else {
  96. newDate = reconstructDateFromFields(resolutions, oldDate);
  97. }
  98. boolean parseErrorWasSet = currentParseErrorMessage != null;
  99. hasChanges |= !Objects.equals(dateString, newDateString)
  100. || !Objects.equals(oldDate, newDate)
  101. || parseErrorWasSet;
  102. if (hasChanges) {
  103. dateString = newDateString;
  104. currentParseErrorMessage = null;
  105. if (newDateString == null || newDateString.isEmpty()) {
  106. boolean valueChanged = setValue(newDate, true);
  107. if (!valueChanged && parseErrorWasSet) {
  108. doSetValue(newDate);
  109. }
  110. } else {
  111. // invalid date string
  112. if (resolutions.isEmpty()) {
  113. Result<T> parsedDate = handleUnparsableDateString(
  114. dateString);
  115. // If handleUnparsableDateString returns the same
  116. // date as current, force update state to display
  117. // correct representation
  118. parsedDate.ifOk(v -> {
  119. if (!setValue(v, true)
  120. && !isDifferentValue(v)) {
  121. updateDiffstate("resolutions",
  122. Json.createObject());
  123. doSetValue(v);
  124. }
  125. });
  126. if (parsedDate.isError()) {
  127. dateString = null;
  128. currentParseErrorMessage = parsedDate
  129. .getMessage().orElse("Parsing error");
  130. if (!isDifferentValue(null)) {
  131. doSetValue(null);
  132. } else {
  133. setValue(null, true);
  134. }
  135. }
  136. } else {
  137. setValue(newDate, true);
  138. }
  139. }
  140. }
  141. }
  142. }
  143. @Override
  144. public void focus() {
  145. fireEvent(new FocusEvent(AbstractDateField.this));
  146. }
  147. @Override
  148. public void blur() {
  149. fireEvent(new BlurEvent(AbstractDateField.this));
  150. }
  151. };
  152. /**
  153. * Value of the field.
  154. */
  155. private T value;
  156. /**
  157. * Default value of the field, displayed when nothing has been selected.
  158. *
  159. * @since 8.1.2
  160. */
  161. private T defaultValue;
  162. /**
  163. * Specified smallest modifiable unit for the date field.
  164. */
  165. private R resolution;
  166. private ZoneId zoneId;
  167. private String dateString = "";
  168. private String currentParseErrorMessage;
  169. private String defaultParseErrorMessage = "Date format not recognized";
  170. private String dateOutOfRangeMessage = "Date is out of allowed range";
  171. /* Constructors */
  172. /**
  173. * Constructs an empty {@code AbstractDateField} with no caption and
  174. * specified {@code resolution}.
  175. *
  176. * @param resolution
  177. * initial resolution for the field, not {@code null}
  178. */
  179. public AbstractDateField(R resolution) {
  180. registerRpc(rpc);
  181. setResolution(resolution);
  182. }
  183. /**
  184. * Constructs an empty {@code AbstractDateField} with caption.
  185. *
  186. * @param caption
  187. * the caption of the datefield
  188. * @param resolution
  189. * initial resolution for the field, not {@code null}
  190. */
  191. public AbstractDateField(String caption, R resolution) {
  192. this(resolution);
  193. setCaption(caption);
  194. }
  195. /**
  196. * Constructs a new {@code AbstractDateField} with the given caption and
  197. * initial text contents.
  198. *
  199. * @param caption
  200. * the caption {@code String} for the editor.
  201. * @param value
  202. * the date/time value.
  203. * @param resolution
  204. * initial resolution for the field, not {@code null}
  205. */
  206. public AbstractDateField(String caption, T value, R resolution) {
  207. this(caption, resolution);
  208. setValue(value);
  209. }
  210. /* Component basic features */
  211. @Override
  212. public void beforeClientResponse(boolean initial) {
  213. super.beforeClientResponse(initial);
  214. Locale locale = getLocale();
  215. getState().locale = locale == null ? null : locale.toString();
  216. }
  217. /**
  218. * Construct a date object from the individual field values received from
  219. * the client.
  220. *
  221. * @param resolutions
  222. * map of time unit (resolution) name and value, the key is the
  223. * resolution name e.g. "HOUR", "MINUTE", the value can be
  224. * {@code null}
  225. * @param oldDate
  226. * used as a fallback to get needed values if they are not
  227. * defined in the specified {@code resolutions}
  228. *
  229. * @return the date object built from the specified resolutions
  230. * @since 8.2
  231. */
  232. protected T reconstructDateFromFields(Map<String, Integer> resolutions,
  233. T oldDate) {
  234. Map<R, Integer> calendarFields = new HashMap<>();
  235. for (R resolution : getResolutionsHigherOrEqualTo(getResolution())) {
  236. // Only handle what the client is allowed to send. The same
  237. // resolutions that are painted
  238. String resolutionName = resolution.name();
  239. Integer newValue = resolutions.get(resolutionName);
  240. if (newValue == null) {
  241. newValue = getDatePart(oldDate, resolution);
  242. }
  243. calendarFields.put(resolution, newValue);
  244. }
  245. return buildDate(calendarFields);
  246. }
  247. /**
  248. * Sets the start range for this component. If the value is set before this
  249. * date (taking the resolution into account), the component will not
  250. * validate. If {@code startDate} is set to {@code null}, any value before
  251. * {@code endDate} will be accepted by the range
  252. *
  253. * @param startDate
  254. * - the allowed range's start date
  255. */
  256. public void setRangeStart(T startDate) {
  257. if (afterDate(startDate, convertFromDateString(getState().rangeEnd))) {
  258. throw new IllegalStateException(
  259. "startDate cannot be later than endDate");
  260. }
  261. getState().rangeStart = convertToDateString(startDate);
  262. }
  263. /**
  264. * Sets the current error message if the range validation fails.
  265. *
  266. * @param dateOutOfRangeMessage
  267. * - Localizable message which is shown when value (the date) is
  268. * set outside allowed range
  269. */
  270. public void setDateOutOfRangeMessage(String dateOutOfRangeMessage) {
  271. this.dateOutOfRangeMessage = dateOutOfRangeMessage;
  272. }
  273. /**
  274. * Returns current date-out-of-range error message.
  275. *
  276. * @see #setDateOutOfRangeMessage(String)
  277. * @return Current error message for dates out of range.
  278. */
  279. public String getDateOutOfRangeMessage() {
  280. return dateOutOfRangeMessage;
  281. }
  282. /**
  283. * Gets the resolution.
  284. *
  285. * @return the date/time field resolution
  286. */
  287. public R getResolution() {
  288. return resolution;
  289. }
  290. /**
  291. * Sets the resolution of the DateField.
  292. *
  293. * The default resolution is {@link DateResolution#DAY} since Vaadin 7.0.
  294. *
  295. * @param resolution
  296. * the resolution to set, not {@code null}
  297. */
  298. public void setResolution(R resolution) {
  299. this.resolution = resolution;
  300. updateResolutions();
  301. }
  302. /**
  303. * Sets the end range for this component. If the value is set after this
  304. * date (taking the resolution into account), the component will not
  305. * validate. If {@code endDate} is set to {@code null}, any value after
  306. * {@code startDate} will be accepted by the range.
  307. *
  308. * @param endDate
  309. * the allowed range's end date (inclusive, based on the current
  310. * resolution)
  311. */
  312. public void setRangeEnd(T endDate) {
  313. String date = convertToDateString(endDate);
  314. if (afterDate(convertFromDateString(getState().rangeStart), endDate)) {
  315. throw new IllegalStateException(
  316. "endDate cannot be earlier than startDate");
  317. }
  318. getState().rangeEnd = date;
  319. }
  320. /**
  321. * Returns the precise rangeStart used.
  322. *
  323. * @return the precise rangeStart used, may be {@code null}.
  324. */
  325. public T getRangeStart() {
  326. return convertFromDateString(getState(false).rangeStart);
  327. }
  328. /**
  329. * Parses string representaion of date range limit into date type
  330. *
  331. * @param temporalStr
  332. * the string representation
  333. * @return parsed value
  334. * @see AbstractDateFieldState#rangeStart
  335. * @see AbstractDateFieldState#rangeEnd
  336. * @since 8.4
  337. */
  338. protected T convertFromDateString(String temporalStr) {
  339. if (temporalStr == null) {
  340. return null;
  341. }
  342. return toType(RANGE_FORMATTER.parse(temporalStr));
  343. }
  344. /**
  345. * Converts a temporal value into field-specific data type.
  346. *
  347. * @param temporalAccessor
  348. * - source value
  349. * @return conversion result.
  350. * @since 8.4
  351. */
  352. protected abstract T toType(TemporalAccessor temporalAccessor);
  353. /**
  354. * Converts date range limit into string representation.
  355. *
  356. * @param temporal
  357. * the value
  358. * @return textual representation
  359. * @see AbstractDateFieldState#rangeStart
  360. * @see AbstractDateFieldState#rangeEnd
  361. * @since 8.4
  362. */
  363. protected String convertToDateString(T temporal) {
  364. if (temporal == null) {
  365. return null;
  366. }
  367. return RANGE_FORMATTER.format(temporal);
  368. }
  369. /**
  370. * Checks if {@code value} is after {@code base} or not.
  371. *
  372. * @param value
  373. * temporal value
  374. * @param base
  375. * temporal value to compare to
  376. * @return {@code true} if {@code value} is after {@code base},
  377. * {@code false} otherwise
  378. */
  379. protected boolean afterDate(T value, T base) {
  380. if (value == null || base == null) {
  381. return false;
  382. }
  383. return value.compareTo(base) > 0;
  384. }
  385. /**
  386. * Returns the precise rangeEnd used.
  387. *
  388. * @return the precise rangeEnd used, may be {@code null}.
  389. */
  390. public T getRangeEnd() {
  391. return convertFromDateString(getState(false).rangeEnd);
  392. }
  393. /**
  394. * Sets formatting used by some component implementations. See
  395. * {@link SimpleDateFormat} for format details.
  396. *
  397. * By default it is encouraged to used default formatting defined by Locale,
  398. * but due some JVM bugs it is sometimes necessary to use this method to
  399. * override formatting. See Vaadin issue #2200.
  400. *
  401. * @param dateFormat
  402. * the dateFormat to set, can be {@code null}
  403. *
  404. * @see com.vaadin.ui.AbstractComponent#setLocale(Locale))
  405. */
  406. public void setDateFormat(String dateFormat) {
  407. getState().format = dateFormat;
  408. }
  409. /**
  410. * Returns a format string used to format date value on client side or
  411. * {@code null} if default formatting from {@link Component#getLocale()} is
  412. * used.
  413. *
  414. * @return the dateFormat
  415. */
  416. public String getDateFormat() {
  417. return getState(false).format;
  418. }
  419. /**
  420. * Sets the {@link ZoneId}, which is used when {@code z} is included inside
  421. * the {@link #setDateFormat(String)}.
  422. *
  423. * @param zoneId
  424. * the zone id
  425. * @since 8.2
  426. */
  427. public void setZoneId(ZoneId zoneId) {
  428. if (zoneId != this.zoneId
  429. || (zoneId != null && !zoneId.equals(this.zoneId))) {
  430. updateTimeZoneJSON(zoneId, getLocale());
  431. }
  432. this.zoneId = zoneId;
  433. }
  434. private void updateTimeZoneJSON(ZoneId zoneId, Locale locale) {
  435. String timeZoneJSON;
  436. if (zoneId != null && locale != null) {
  437. timeZoneJSON = TimeZoneUtil.toJSON(zoneId, locale);
  438. } else {
  439. timeZoneJSON = null;
  440. }
  441. getState().timeZoneJSON = timeZoneJSON;
  442. }
  443. @Override
  444. public void setLocale(Locale locale) {
  445. Locale oldLocale = getLocale();
  446. if (locale != oldLocale
  447. || (locale != null && !locale.equals(oldLocale))) {
  448. updateTimeZoneJSON(getZoneId(), locale);
  449. }
  450. super.setLocale(locale);
  451. }
  452. private void updateResolutions() {
  453. final T currentDate = getValue();
  454. Map<String, Integer> resolutions = getState().resolutions;
  455. resolutions.clear();
  456. // Only paint variables for the resolution and up, e.g. Resolution DAY
  457. // paints DAY,MONTH,YEAR
  458. for (R resolution : getResolutionsHigherOrEqualTo(getResolution())) {
  459. String resolutionName = resolution.name();
  460. Integer value = getValuePart(currentDate, resolution);
  461. resolutions.put(resolutionName, value);
  462. Integer defaultValuePart = getValuePart(defaultValue, resolution);
  463. resolutions.put("default-" + resolutionName, defaultValuePart);
  464. }
  465. }
  466. private Integer getValuePart(T date, R resolution) {
  467. if (date == null) {
  468. return null;
  469. }
  470. return getDatePart(date, resolution);
  471. }
  472. /**
  473. * Returns the {@link ZoneId}, which is used when {@code z} is included
  474. * inside the {@link #setDateFormat(String)}.
  475. *
  476. * @return the zoneId
  477. * @since 8.2
  478. */
  479. public ZoneId getZoneId() {
  480. return zoneId;
  481. }
  482. /**
  483. * Specifies whether or not date/time interpretation in component is to be
  484. * lenient.
  485. *
  486. * @see Calendar#setLenient(boolean)
  487. * @see #isLenient()
  488. *
  489. * @param lenient
  490. * true if the lenient mode is to be turned on; false if it is to
  491. * be turned off.
  492. */
  493. public void setLenient(boolean lenient) {
  494. getState().lenient = lenient;
  495. }
  496. /**
  497. * Returns whether date/time interpretation is lenient.
  498. *
  499. * @see #setLenient(boolean)
  500. *
  501. * @return {@code true} if the interpretation mode of this calendar is
  502. * lenient; {@code false} otherwise.
  503. */
  504. public boolean isLenient() {
  505. return getState(false).lenient;
  506. }
  507. @Override
  508. public T getValue() {
  509. return value;
  510. }
  511. /**
  512. * Returns the current default value.
  513. *
  514. * @see #setDefaultValue(Temporal)
  515. * @return the default value
  516. * @since 8.1.2
  517. */
  518. public T getDefaultValue() {
  519. return defaultValue;
  520. }
  521. /**
  522. * Sets the default value for the field. The default value is the starting
  523. * point for the date field when nothing has been selected yet. If no
  524. * default value is set, current date/time is used.
  525. *
  526. * @param defaultValue
  527. * the default value, may be {@code null}
  528. * @since 8.1.2
  529. */
  530. public void setDefaultValue(T defaultValue) {
  531. this.defaultValue = defaultValue;
  532. updateResolutions();
  533. }
  534. /**
  535. * Sets the value of this object. If the new value is not equal to
  536. * {@code getValue()}, fires a {@link ValueChangeEvent} .
  537. *
  538. * @param value
  539. * the new value, may be {@code null}
  540. */
  541. @Override
  542. public void setValue(T value) {
  543. currentParseErrorMessage = null;
  544. /*
  545. * First handle special case when the client side component have a date
  546. * string but value is null (e.g. unparsable date string typed in by the
  547. * user). No value changes should happen, but we need to do some
  548. * internal housekeeping.
  549. */
  550. if (value == null && !getState(false).parsable) {
  551. /*
  552. * Side-effects of doSetValue clears possible previous strings and
  553. * flags about invalid input.
  554. */
  555. doSetValue(null);
  556. markAsDirty();
  557. return;
  558. }
  559. super.setValue(value);
  560. }
  561. /**
  562. * Checks whether ISO 8601 week numbers are shown in the date selector.
  563. *
  564. * @return true if week numbers are shown, false otherwise.
  565. */
  566. public boolean isShowISOWeekNumbers() {
  567. return getState(false).showISOWeekNumbers;
  568. }
  569. /**
  570. * Sets the visibility of ISO 8601 week numbers in the date selector. ISO
  571. * 8601 defines that a week always starts with a Monday so the week numbers
  572. * are only shown if this is the case.
  573. *
  574. * @param showWeekNumbers
  575. * true if week numbers should be shown, false otherwise.
  576. */
  577. public void setShowISOWeekNumbers(boolean showWeekNumbers) {
  578. getState().showISOWeekNumbers = showWeekNumbers;
  579. }
  580. /**
  581. * Return the error message that is shown if the user inputted value can't
  582. * be parsed into a Date object. If
  583. * {@link #handleUnparsableDateString(String)} is overridden and it throws a
  584. * custom exception, the message returned by
  585. * {@link Exception#getLocalizedMessage()} will be used instead of the value
  586. * returned by this method.
  587. *
  588. * @see #setParseErrorMessage(String)
  589. *
  590. * @return the error message that the DateField uses when it can't parse the
  591. * textual input from user to a Date object
  592. */
  593. public String getParseErrorMessage() {
  594. return defaultParseErrorMessage;
  595. }
  596. /**
  597. * Sets the default error message used if the DateField cannot parse the
  598. * text input by user to a Date field. Note that if the
  599. * {@link #handleUnparsableDateString(String)} method is overridden, the
  600. * localized message from its exception is used.
  601. *
  602. * @param parsingErrorMessage
  603. * the default parsing error message
  604. *
  605. * @see #getParseErrorMessage()
  606. * @see #handleUnparsableDateString(String)
  607. */
  608. public void setParseErrorMessage(String parsingErrorMessage) {
  609. defaultParseErrorMessage = parsingErrorMessage;
  610. }
  611. @Override
  612. public Registration addFocusListener(FocusListener listener) {
  613. return addListener(FocusEvent.EVENT_ID, FocusEvent.class, listener,
  614. FocusListener.focusMethod);
  615. }
  616. @Override
  617. public Registration addBlurListener(BlurListener listener) {
  618. return addListener(BlurEvent.EVENT_ID, BlurEvent.class, listener,
  619. BlurListener.blurMethod);
  620. }
  621. @Override
  622. @SuppressWarnings("unchecked")
  623. public void readDesign(Element design, DesignContext designContext) {
  624. super.readDesign(design, designContext);
  625. if (design.hasAttr("value") && !design.attr("value").isEmpty()) {
  626. Type dateType = GenericTypeReflector.getTypeParameter(getClass(),
  627. AbstractDateField.class.getTypeParameters()[0]);
  628. if (dateType instanceof Class<?>) {
  629. Class<?> clazz = (Class<?>) dateType;
  630. T date = (T) DesignAttributeHandler.getFormatter()
  631. .parse(design.attr("value"), clazz);
  632. // formatting will return null if it cannot parse the string
  633. if (date == null) {
  634. Logger.getLogger(AbstractDateField.class.getName())
  635. .info("cannot parse " + design.attr("value")
  636. + " as date");
  637. }
  638. doSetValue(date);
  639. } else {
  640. throw new RuntimeException("Cannot detect resoluton type "
  641. + Optional.ofNullable(dateType).map(Type::getTypeName)
  642. .orElse(null));
  643. }
  644. }
  645. }
  646. /**
  647. * Formats date according to the components locale.
  648. *
  649. * @param value
  650. * the date or {@code null}
  651. * @return textual representation of the date or empty string for
  652. * {@code null}
  653. * @since 8.1.1
  654. */
  655. protected abstract String formatDate(T value);
  656. @Override
  657. public void writeDesign(Element design, DesignContext designContext) {
  658. super.writeDesign(design, designContext);
  659. if (getValue() != null) {
  660. design.attr("value",
  661. DesignAttributeHandler.getFormatter().format(getValue()));
  662. }
  663. }
  664. /**
  665. * This method is called to handle a non-empty date string from the client
  666. * if the client could not parse it as a Date.
  667. *
  668. * By default, an error result is returned whose error message is
  669. * {@link #getParseErrorMessage()}.
  670. *
  671. * This can be overridden to handle conversions, to return a result with
  672. * {@code null} value (equivalent to empty input) or to return a custom
  673. * error.
  674. *
  675. * @param dateString
  676. * date string to handle
  677. * @return result that contains parsed Date as a value or an error
  678. */
  679. protected Result<T> handleUnparsableDateString(String dateString) {
  680. return Result.error(getParseErrorMessage());
  681. }
  682. @Override
  683. protected AbstractDateFieldState getState() {
  684. return (AbstractDateFieldState) super.getState();
  685. }
  686. @Override
  687. protected AbstractDateFieldState getState(boolean markAsDirty) {
  688. return (AbstractDateFieldState) super.getState(markAsDirty);
  689. }
  690. @Override
  691. protected void doSetValue(T value) {
  692. this.value = value;
  693. // Also set the internal dateString
  694. if (value == null) {
  695. value = getEmptyValue();
  696. }
  697. dateString = formatDate(value);
  698. // TODO move range check to internal validator?
  699. RangeValidator<T> validator = getRangeValidator();
  700. ValidationResult result = validator.apply(value,
  701. new ValueContext(this, this));
  702. if (result.isError()) {
  703. currentParseErrorMessage = getDateOutOfRangeMessage();
  704. }
  705. getState().parsable = currentParseErrorMessage == null;
  706. ErrorMessage errorMessage;
  707. if (currentParseErrorMessage == null) {
  708. errorMessage = null;
  709. } else {
  710. errorMessage = new UserError(currentParseErrorMessage);
  711. }
  712. setComponentError(errorMessage);
  713. updateResolutions();
  714. }
  715. /**
  716. * Returns a date integer value part for the given {@code date} for the
  717. * given {@code resolution}.
  718. *
  719. * @param date
  720. * the given date, can be {@code null}
  721. * @param resolution
  722. * the resolution to extract a value from the date by, not
  723. * {@code null}
  724. * @return the integer value part of the date by the given resolution
  725. */
  726. protected abstract int getDatePart(T date, R resolution);
  727. /**
  728. * Builds date by the given {@code resolutionValues} which is a map whose
  729. * keys are resolution and integer values.
  730. * <p>
  731. * This is the opposite to {@link #getDatePart(Temporal, Enum)}.
  732. *
  733. * @param resolutionValues
  734. * date values to construct a date
  735. * @return date built from the given map of date values
  736. */
  737. protected abstract T buildDate(Map<R, Integer> resolutionValues);
  738. /**
  739. * Returns a custom date range validator which is applicable for the type
  740. * {@code T}.
  741. *
  742. * @return the date range validator
  743. */
  744. protected abstract RangeValidator<T> getRangeValidator();
  745. /**
  746. * Converts {@link Date} to date type {@code T}.
  747. *
  748. * @param date
  749. * a date to convert
  750. * @return object of type {@code T} representing the {@code date}
  751. */
  752. protected abstract T convertFromDate(Date date);
  753. /**
  754. * Converts the object of type {@code T} to {@link Date}.
  755. * <p>
  756. * This is the opposite to {@link #convertFromDate(Date)}.
  757. *
  758. * @param date
  759. * the date of type {@code T}
  760. * @return converted date of type {@code Date}
  761. */
  762. protected abstract Date convertToDate(T date);
  763. @SuppressWarnings("unchecked")
  764. private Stream<R> getResolutions() {
  765. Type resolutionType = GenericTypeReflector.getTypeParameter(getClass(),
  766. AbstractDateField.class.getTypeParameters()[1]);
  767. if (resolutionType instanceof Class<?>) {
  768. Class<?> clazz = (Class<?>) resolutionType;
  769. return Stream.of(clazz.getEnumConstants())
  770. .map(object -> (R) object);
  771. }
  772. throw new RuntimeException("Cannot detect resoluton type "
  773. + Optional.ofNullable(resolutionType).map(Type::getTypeName)
  774. .orElse(null));
  775. }
  776. private Iterable<R> getResolutionsHigherOrEqualTo(R resoution) {
  777. return getResolutions().skip(resolution.ordinal())
  778. .collect(Collectors.toList());
  779. }
  780. @Override
  781. public Validator<T> getDefaultValidator() {
  782. return new Validator<T>() {
  783. @Override
  784. public ValidationResult apply(T value, ValueContext context) {
  785. if (currentParseErrorMessage != null) {
  786. return ValidationResult.error(currentParseErrorMessage);
  787. }
  788. // Pass to range validator.
  789. return getRangeValidator().apply(value, context);
  790. }
  791. };
  792. }
  793. /**
  794. * <p>
  795. * Sets a custom style name for the given date's calendar cell. Setting the
  796. * style name will override any previous style names that have been set for
  797. * that date, but can contain several actual style names separated by space.
  798. * Setting the custom style name {@code null} will only remove the previous
  799. * custom style name.
  800. * </p>
  801. * <p>
  802. * This logic is entirely separate from {@link #setStyleName(String)}
  803. * </p>
  804. * <p>
  805. * Usage examples: <br>
  806. * {@code setDateStyle(LocalDate.now(), "teststyle");} <br>
  807. * {@code setDateStyle(LocalDate.now(), "teststyle1 teststyle2");}
  808. * </p>
  809. *
  810. * @param date
  811. * which date cell to modify, not {@code null}
  812. * @param styleName
  813. * the custom style name(s) for given date, {@code null} to clear
  814. * custom style name(s)
  815. *
  816. * @since 8.3
  817. */
  818. public void setDateStyle(LocalDate date, String styleName) {
  819. Objects.requireNonNull(date, "Date cannot be null");
  820. if (styleName != null) {
  821. getState().dateStyles.put(date.toString(), styleName);
  822. } else {
  823. getState().dateStyles.remove(date.toString());
  824. }
  825. }
  826. /**
  827. * Returns the custom style name that corresponds with the given date's
  828. * calendar cell.
  829. *
  830. * @param date
  831. * which date cell's custom style name(s) to return, not
  832. * {@code null}
  833. * @return the corresponding style name(s), if any, {@code null} otherwise
  834. *
  835. * @see #setDateStyle(LocalDate, String)
  836. * @since 8.3
  837. */
  838. public String getDateStyle(LocalDate date) {
  839. Objects.requireNonNull(date, "Date cannot be null");
  840. return getState(false).dateStyles.get(date.toString());
  841. }
  842. /**
  843. * Returns a map from dates to custom style names in each date's calendar
  844. * cell.
  845. *
  846. * @return unmodifiable map from dates to custom style names in each date's
  847. * calendar cell
  848. *
  849. * @see #setDateStyle(LocalDate, String)
  850. * @since 8.3
  851. */
  852. public Map<LocalDate, String> getDateStyles() {
  853. HashMap<LocalDate, String> hashMap = new HashMap<>();
  854. for (Entry<String, String> entry : getState(false).dateStyles
  855. .entrySet()) {
  856. hashMap.put(LocalDate.parse(entry.getKey()), entry.getValue());
  857. }
  858. return Collections.unmodifiableMap(hashMap);
  859. }
  860. /**
  861. * Sets the assistive label for a calendar navigation element. This sets the
  862. * {@code aria-label} attribute for the element which is used by screen
  863. * reading software.
  864. *
  865. * @param element
  866. * the element for which to set the label. Not {@code null}.
  867. * @param label
  868. * the assistive label to set
  869. * @since 8.4
  870. */
  871. public void setAssistiveLabel(AccessibleElement element, String label) {
  872. Objects.requireNonNull(element, "Element cannot be null");
  873. getState().assistiveLabels.put(element, label);
  874. }
  875. /**
  876. * Gets the assistive label of a calendar navigation element.
  877. *
  878. * @param element
  879. * the element of which to get the assistive label
  880. * @since 8.4
  881. */
  882. public void getAssistiveLabel(AccessibleElement element) {
  883. getState(false).assistiveLabels.get(element);
  884. }
  885. }