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 23KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689
  1. /*
  2. * Copyright 2000-2016 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.temporal.Temporal;
  22. import java.time.temporal.TemporalAdjuster;
  23. import java.util.Calendar;
  24. import java.util.Date;
  25. import java.util.HashMap;
  26. import java.util.Locale;
  27. import java.util.Map;
  28. import java.util.Objects;
  29. import java.util.Optional;
  30. import java.util.Set;
  31. import java.util.logging.Logger;
  32. import java.util.stream.Collectors;
  33. import java.util.stream.Stream;
  34. import org.jsoup.nodes.Element;
  35. import com.googlecode.gentyref.GenericTypeReflector;
  36. import com.vaadin.data.Result;
  37. import com.vaadin.data.ValidationResult;
  38. import com.vaadin.data.Validator;
  39. import com.vaadin.data.ValueContext;
  40. import com.vaadin.data.validator.RangeValidator;
  41. import com.vaadin.event.FieldEvents.BlurEvent;
  42. import com.vaadin.event.FieldEvents.BlurListener;
  43. import com.vaadin.event.FieldEvents.BlurNotifier;
  44. import com.vaadin.event.FieldEvents.FocusEvent;
  45. import com.vaadin.event.FieldEvents.FocusListener;
  46. import com.vaadin.event.FieldEvents.FocusNotifier;
  47. import com.vaadin.server.PaintException;
  48. import com.vaadin.server.PaintTarget;
  49. import com.vaadin.server.UserError;
  50. import com.vaadin.shared.Registration;
  51. import com.vaadin.shared.ui.datefield.AbstractDateFieldState;
  52. import com.vaadin.shared.ui.datefield.DateFieldConstants;
  53. import com.vaadin.shared.ui.datefield.DateResolution;
  54. import com.vaadin.ui.declarative.DesignAttributeHandler;
  55. import com.vaadin.ui.declarative.DesignContext;
  56. /**
  57. * A date editor component with {@link LocalDate} as an input value.
  58. *
  59. * @param <T> type of date ({@code LocalDate} or {@code LocalDateTime}).
  60. * @param <R> resolution enumeration type
  61. * @author Vaadin Ltd
  62. * @since 8.0
  63. */
  64. public abstract class AbstractDateField<T extends Temporal & TemporalAdjuster & Serializable & Comparable<? super T>, R extends Enum<R>>
  65. extends AbstractField<T>
  66. implements LegacyComponent, FocusNotifier, BlurNotifier {
  67. /**
  68. * Value of the field.
  69. */
  70. private T value;
  71. /**
  72. * Specified smallest modifiable unit for the date field.
  73. */
  74. private R resolution;
  75. /**
  76. * Overridden format string
  77. */
  78. private String dateFormat;
  79. private boolean lenient = false;
  80. private String dateString = "";
  81. private String currentParseErrorMessage;
  82. /**
  83. * Was the last entered string parsable? If this flag is false, datefields
  84. * internal validator does not pass.
  85. */
  86. private boolean uiHasValidDateString = true;
  87. /**
  88. * Determines if week numbers are shown in the date selector.
  89. */
  90. private boolean showISOWeekNumbers = false;
  91. private String defaultParseErrorMessage = "Date format not recognized";
  92. private String dateOutOfRangeMessage = "Date is out of allowed range";
  93. /* Constructors */
  94. /**
  95. * Constructs an empty <code>AbstractDateField</code> with no caption and
  96. * specified {@code resolution}.
  97. *
  98. * @param resolution initial resolution for the field
  99. */
  100. public AbstractDateField(R resolution) {
  101. this.resolution = resolution;
  102. }
  103. /**
  104. * Constructs an empty <code>AbstractDateField</code> with caption.
  105. *
  106. * @param caption the caption of the datefield.
  107. * @param resolution initial resolution for the field
  108. */
  109. public AbstractDateField(String caption, R resolution) {
  110. this(resolution);
  111. setCaption(caption);
  112. }
  113. /**
  114. * Constructs a new <code>AbstractDateField</code> with the given caption
  115. * and initial text contents.
  116. *
  117. * @param caption the caption <code>String</code> for the editor.
  118. * @param value the date/time value.
  119. * @param resolution initial resolution for the field
  120. */
  121. public AbstractDateField(String caption, T value, R resolution) {
  122. this(caption, resolution);
  123. setValue(value);
  124. }
  125. /* Component basic features */
  126. /*
  127. * Paints this component. Don't add a JavaDoc comment here, we use the
  128. * default documentation from implemented interface.
  129. */
  130. @Override
  131. public void paintContent(PaintTarget target) throws PaintException {
  132. // Adds the locale as attribute
  133. final Locale l = getLocale();
  134. if (l != null) {
  135. target.addAttribute("locale", l.toString());
  136. }
  137. if (getDateFormat() != null) {
  138. target.addAttribute("format", getDateFormat());
  139. }
  140. if (!isLenient()) {
  141. target.addAttribute("strict", true);
  142. }
  143. target.addAttribute(DateFieldConstants.ATTR_WEEK_NUMBERS,
  144. isShowISOWeekNumbers());
  145. target.addAttribute("parsable", uiHasValidDateString);
  146. final T currentDate = getValue();
  147. // Only paint variables for the resolution and up, e.g. Resolution DAY
  148. // paints DAY,MONTH,YEAR
  149. for (R res : getResolutionsHigherOrEqualTo(getResolution())) {
  150. int value = -1;
  151. if (currentDate != null) {
  152. value = getDatePart(currentDate, res);
  153. }
  154. target.addVariable(this, getResolutionVariable(res), value);
  155. }
  156. }
  157. /*
  158. * Invoked when a variable of the component changes. Don't add a JavaDoc
  159. * comment here, we use the default documentation from implemented
  160. * interface.
  161. */
  162. @Override
  163. public void changeVariables(Object source, Map<String, Object> variables) {
  164. Set<String> resolutionNames = getResolutions()
  165. .map(this::getResolutionVariable).collect(Collectors.toSet());
  166. resolutionNames.retainAll(variables.keySet());
  167. if (!isReadOnly() && (!resolutionNames.isEmpty()
  168. || variables.containsKey("dateString"))) {
  169. // Old and new dates
  170. final T oldDate = getValue();
  171. // this enables analyzing invalid input on the server
  172. final String newDateString = (String) variables.get("dateString");
  173. T newDate;
  174. if ("".equals(newDateString)) {
  175. newDate = null;
  176. uiHasValidDateString = true;
  177. currentParseErrorMessage = null;
  178. } else {
  179. newDate = reconstructDateFromFields(variables, oldDate);
  180. }
  181. boolean hasChanges = !Objects.equals(dateString, newDateString) ||
  182. !Objects.equals(oldDate, newDate);
  183. if (hasChanges) {
  184. dateString = newDateString;
  185. if (newDateString != null && !newDateString.isEmpty()) {
  186. String invalidDateString = (String) variables.get("lastInvalidDateString");
  187. if (invalidDateString != null) {
  188. Result<T> parsedDate = handleUnparsableDateString(dateString);
  189. parsedDate.ifOk(this::setValue);
  190. if (parsedDate.isError()) {
  191. uiHasValidDateString = true;
  192. currentParseErrorMessage = parsedDate.getMessage().orElse("Parsing error");
  193. setComponentError(new UserError(getParseErrorMessage()));
  194. }
  195. } else {
  196. setValue(newDate, true);
  197. }
  198. } else {
  199. setValue(newDate, true);
  200. }
  201. markAsDirty();
  202. }
  203. }
  204. if (variables.containsKey(FocusEvent.EVENT_ID)) {
  205. fireEvent(new FocusEvent(this));
  206. }
  207. if (variables.containsKey(BlurEvent.EVENT_ID)) {
  208. fireEvent(new BlurEvent(this));
  209. }
  210. }
  211. protected T reconstructDateFromFields(Map<String, Object> variables, T oldDate) {
  212. Map<R, Integer> calendarFields = new HashMap<>();
  213. for (R resolution : getResolutionsHigherOrEqualTo(
  214. getResolution())) {
  215. // Only handle what the client is allowed to send. The same
  216. // resolutions that are painted
  217. String variableName = getResolutionVariable(resolution);
  218. Integer newValue = (Integer) variables.get(variableName);
  219. if (newValue != null && newValue >= 0) {
  220. calendarFields.put(resolution, newValue);
  221. } else {
  222. calendarFields.put(resolution, getDatePart(oldDate, resolution));
  223. }
  224. }
  225. return buildDate(calendarFields);
  226. }
  227. /**
  228. * Sets the start range for this component. If the value is set before this
  229. * date (taking the resolution into account), the component will not
  230. * validate. If <code>startDate</code> is set to <code>null</code>, any
  231. * value before <code>endDate</code> will be accepted by the range
  232. *
  233. * @param startDate - the allowed range's start date
  234. */
  235. public void setRangeStart(T startDate) {
  236. Date date = convertToDate(startDate);
  237. if (date != null && getState().rangeEnd != null
  238. && date.after(getState().rangeEnd)) {
  239. throw new IllegalStateException(
  240. "startDate cannot be later than endDate");
  241. }
  242. getState().rangeStart = date;
  243. }
  244. /**
  245. * Sets the current error message if the range validation fails.
  246. *
  247. * @param dateOutOfRangeMessage - Localizable message which is shown when value (the date) is
  248. * set outside allowed range
  249. */
  250. public void setDateOutOfRangeMessage(String dateOutOfRangeMessage) {
  251. this.dateOutOfRangeMessage = dateOutOfRangeMessage;
  252. }
  253. /**
  254. * Returns current date-out-of-range error message.
  255. *
  256. * @return Current error message for dates out of range.
  257. * @see #setDateOutOfRangeMessage(String)
  258. */
  259. public String getDateOutOfRangeMessage() {
  260. return dateOutOfRangeMessage;
  261. }
  262. /**
  263. * Gets the resolution.
  264. *
  265. * @return the date/time field resolution
  266. */
  267. public R getResolution() {
  268. return resolution;
  269. }
  270. /**
  271. * Sets the resolution of the DateField.
  272. * <p>
  273. * The default resolution is {@link DateResolution#DAY} since Vaadin 7.0.
  274. *
  275. * @param resolution the resolution to set, not {@code null}
  276. */
  277. public void setResolution(R resolution) {
  278. this.resolution = resolution;
  279. markAsDirty();
  280. }
  281. /**
  282. * Sets the end range for this component. If the value is set after this
  283. * date (taking the resolution into account), the component will not
  284. * validate. If <code>endDate</code> is set to <code>null</code>, any value
  285. * after <code>startDate</code> will be accepted by the range.
  286. *
  287. * @param endDate - the allowed range's end date (inclusive, based on the
  288. * current resolution)
  289. */
  290. public void setRangeEnd(T endDate) {
  291. Date date = convertToDate(endDate);
  292. if (date != null && getState().rangeStart != null
  293. && getState().rangeStart.after(date)) {
  294. throw new IllegalStateException(
  295. "endDate cannot be earlier than startDate");
  296. }
  297. getState().rangeEnd = date;
  298. }
  299. /**
  300. * Returns the precise rangeStart used.
  301. *
  302. * @return the precise rangeStart used, may be null.
  303. */
  304. public T getRangeStart() {
  305. return convertFromDate(getState(false).rangeStart);
  306. }
  307. /**
  308. * Returns the precise rangeEnd used.
  309. *
  310. * @return the precise rangeEnd used, may be null.
  311. */
  312. public T getRangeEnd() {
  313. return convertFromDate(getState(false).rangeEnd);
  314. }
  315. /**
  316. * Sets formatting used by some component implementations. See
  317. * {@link SimpleDateFormat} for format details.
  318. * <p>
  319. * By default it is encouraged to used default formatting defined by Locale,
  320. * but due some JVM bugs it is sometimes necessary to use this method to
  321. * override formatting. See Vaadin issue #2200.
  322. *
  323. * @param dateFormat the dateFormat to set
  324. * @see com.vaadin.ui.AbstractComponent#setLocale(Locale))
  325. */
  326. public void setDateFormat(String dateFormat) {
  327. this.dateFormat = dateFormat;
  328. markAsDirty();
  329. }
  330. /**
  331. * Returns a format string used to format date value on client side or null
  332. * if default formatting from {@link Component#getLocale()} is used.
  333. *
  334. * @return the dateFormat
  335. */
  336. public String getDateFormat() {
  337. return dateFormat;
  338. }
  339. /**
  340. * Specifies whether or not date/time interpretation in component is to be
  341. * lenient.
  342. *
  343. * @param lenient true if the lenient mode is to be turned on; false if it is to
  344. * be turned off.
  345. * @see Calendar#setLenient(boolean)
  346. * @see #isLenient()
  347. */
  348. public void setLenient(boolean lenient) {
  349. this.lenient = lenient;
  350. markAsDirty();
  351. }
  352. /**
  353. * Returns whether date/time interpretation is to be lenient.
  354. *
  355. * @return true if the interpretation mode of this calendar is lenient;
  356. * false otherwise.
  357. * @see #setLenient(boolean)
  358. */
  359. public boolean isLenient() {
  360. return lenient;
  361. }
  362. @Override
  363. public T getValue() {
  364. return value;
  365. }
  366. /**
  367. * Sets the value of this object. If the new value is not equal to
  368. * {@code getValue()}, fires a {@link ValueChangeEvent} .
  369. *
  370. * @param value the new value, may be {@code null}
  371. */
  372. @Override
  373. public void setValue(T value) {
  374. /*
  375. * First handle special case when the client side component have a date
  376. * string but value is null (e.g. unparsable date string typed in by the
  377. * user). No value changes should happen, but we need to do some
  378. * internal housekeeping.
  379. */
  380. if (value == null && !uiHasValidDateString) {
  381. /*
  382. * Side-effects of doSetValue clears possible previous strings and
  383. * flags about invalid input.
  384. */
  385. doSetValue(null);
  386. markAsDirty();
  387. return;
  388. }
  389. super.setValue(value);
  390. }
  391. /**
  392. * Checks whether ISO 8601 week numbers are shown in the date selector.
  393. *
  394. * @return true if week numbers are shown, false otherwise.
  395. */
  396. public boolean isShowISOWeekNumbers() {
  397. return showISOWeekNumbers;
  398. }
  399. /**
  400. * Sets the visibility of ISO 8601 week numbers in the date selector. ISO
  401. * 8601 defines that a week always starts with a Monday so the week numbers
  402. * are only shown if this is the case.
  403. *
  404. * @param showWeekNumbers true if week numbers should be shown, false otherwise.
  405. */
  406. public void setShowISOWeekNumbers(boolean showWeekNumbers) {
  407. showISOWeekNumbers = showWeekNumbers;
  408. markAsDirty();
  409. }
  410. /**
  411. * Return the error message that is shown if the user inputted value can't
  412. * be parsed into a Date object. If
  413. * {@link #handleUnparsableDateString(String)} is overridden and it throws a
  414. * custom exception, the message returned by
  415. * {@link Exception#getLocalizedMessage()} will be used instead of the value
  416. * returned by this method.
  417. *
  418. * @return the error message that the DateField uses when it can't parse the
  419. * textual input from user to a Date object
  420. * @see #setParseErrorMessage(String)
  421. */
  422. public String getParseErrorMessage() {
  423. return defaultParseErrorMessage;
  424. }
  425. /**
  426. * Sets the default error message used if the DateField cannot parse the
  427. * text input by user to a Date field. Note that if the
  428. * {@link #handleUnparsableDateString(String)} method is overridden, the
  429. * localized message from its exception is used.
  430. *
  431. * @param parsingErrorMessage
  432. * @see #getParseErrorMessage()
  433. * @see #handleUnparsableDateString(String)
  434. */
  435. public void setParseErrorMessage(String parsingErrorMessage) {
  436. defaultParseErrorMessage = parsingErrorMessage;
  437. }
  438. @Override
  439. public Registration addFocusListener(FocusListener listener) {
  440. return addListener(FocusEvent.EVENT_ID, FocusEvent.class, listener,
  441. FocusListener.focusMethod);
  442. }
  443. @Override
  444. public Registration addBlurListener(BlurListener listener) {
  445. return addListener(BlurEvent.EVENT_ID, BlurEvent.class, listener,
  446. BlurListener.blurMethod);
  447. }
  448. @Override
  449. @SuppressWarnings("unchecked")
  450. public void readDesign(Element design, DesignContext designContext) {
  451. super.readDesign(design, designContext);
  452. if (design.hasAttr("value") && !design.attr("value").isEmpty()) {
  453. Type dateType = GenericTypeReflector.getTypeParameter(getClass(),
  454. AbstractDateField.class.getTypeParameters()[0]);
  455. if (dateType instanceof Class<?>) {
  456. Class<?> clazz = (Class<?>) dateType;
  457. T date = (T) DesignAttributeHandler.getFormatter()
  458. .parse(design.attr("value"), clazz);
  459. // formatting will return null if it cannot parse the string
  460. if (date == null) {
  461. Logger.getLogger(AbstractDateField.class.getName())
  462. .info("cannot parse " + design.attr("value")
  463. + " as date");
  464. }
  465. doSetValue(date);
  466. } else {
  467. throw new RuntimeException("Cannot detect resoluton type "
  468. + Optional.ofNullable(dateType).map(Type::getTypeName)
  469. .orElse(null));
  470. }
  471. }
  472. }
  473. /**
  474. * Formats date according to the components locale.
  475. *
  476. * @param value the date or {@code null}
  477. * @return textual representation of the date or empty string for {@code null}
  478. */
  479. protected abstract String formatDate(T value);
  480. @Override
  481. public void writeDesign(Element design, DesignContext designContext) {
  482. super.writeDesign(design, designContext);
  483. if (getValue() != null) {
  484. design.attr("value",
  485. DesignAttributeHandler.getFormatter().format(getValue()));
  486. }
  487. }
  488. /**
  489. * This method is called to handle a non-empty date string from the client
  490. * if the client could not parse it as a Date.
  491. * <p>
  492. * By default, an error result is returned whose error message is
  493. * {@link #getParseErrorMessage()}.
  494. * <p>
  495. * This can be overridden to handle conversions, to return a result with
  496. * {@code null} value (equivalent to empty input) or to return a custom
  497. * error.
  498. *
  499. * @param dateString date string to handle
  500. * @return result that contains parsed Date as a value or an error
  501. */
  502. protected Result<T> handleUnparsableDateString(String dateString) {
  503. return Result.error(getParseErrorMessage());
  504. }
  505. @Override
  506. protected AbstractDateFieldState getState() {
  507. return (AbstractDateFieldState) super.getState();
  508. }
  509. @Override
  510. protected AbstractDateFieldState getState(boolean markAsDirty) {
  511. return (AbstractDateFieldState) super.getState(markAsDirty);
  512. }
  513. @Override
  514. protected void doSetValue(T value) {
  515. uiHasValidDateString = true;
  516. currentParseErrorMessage = null;
  517. this.value = value;
  518. // Also set the internal dateString
  519. if (value != null) {
  520. dateString = formatDate(value);
  521. } else {
  522. dateString = formatDate(getEmptyValue());
  523. }
  524. RangeValidator<T> validator = getRangeValidator();
  525. ValidationResult result = validator.apply(value,
  526. new ValueContext(this, this));
  527. if (result.isError()) {
  528. currentParseErrorMessage = getDateOutOfRangeMessage();
  529. }
  530. if (currentParseErrorMessage == null) {
  531. setComponentError(null);
  532. } else {
  533. setComponentError(new UserError(currentParseErrorMessage));
  534. }
  535. }
  536. /**
  537. * Returns a date integer value part for the given {@code date} for the
  538. * given {@code resolution}.
  539. *
  540. * @param date the given date
  541. * @param resolution the resolution to extract a value from the date by
  542. * @return the integer value part of the date by the given resolution
  543. */
  544. protected abstract int getDatePart(T date, R resolution);
  545. /**
  546. * Builds date by the given {@code resolutionValues} which is a map whose
  547. * keys are resolution and integer values.
  548. * <p>
  549. * This is the opposite to {@link #getDatePart(Temporal, Enum)}.
  550. *
  551. * @param resolutionValues date values to construct a date
  552. * @return date built from the given map of date values
  553. */
  554. protected abstract T buildDate(Map<R, Integer> resolutionValues);
  555. /**
  556. * Returns a custom date range validator which is applicable for the type
  557. * {@code T}.
  558. *
  559. * @return the date range validator
  560. */
  561. protected abstract RangeValidator<T> getRangeValidator();
  562. /**
  563. * Converts {@link Date} to date type {@code T}.
  564. *
  565. * @param date a date to convert
  566. * @return object of type {@code T} representing the {@code date}
  567. */
  568. protected abstract T convertFromDate(Date date);
  569. /**
  570. * Converts the object of type {@code T} to {@link Date}.
  571. * <p>
  572. * This is the opposite to {@link #convertFromDate(Date)}.
  573. *
  574. * @param date the date of type {@code T}
  575. * @return converted date of type {@code Date}
  576. */
  577. protected abstract Date convertToDate(T date);
  578. private String getResolutionVariable(R resolution) {
  579. return resolution.name().toLowerCase(Locale.ENGLISH);
  580. }
  581. @SuppressWarnings("unchecked")
  582. private Stream<R> getResolutions() {
  583. Type resolutionType = GenericTypeReflector.getTypeParameter(getClass(),
  584. AbstractDateField.class.getTypeParameters()[1]);
  585. if (resolutionType instanceof Class<?>) {
  586. Class<?> clazz = (Class<?>) resolutionType;
  587. return Stream.of(clazz.getEnumConstants())
  588. .map(object -> (R) object);
  589. } else {
  590. throw new RuntimeException("Cannot detect resoluton type "
  591. + Optional.ofNullable(resolutionType).map(Type::getTypeName)
  592. .orElse(null));
  593. }
  594. }
  595. private Iterable<R> getResolutionsHigherOrEqualTo(R resoution) {
  596. return getResolutions().skip(resolution.ordinal())
  597. .collect(Collectors.toList());
  598. }
  599. @Override
  600. public Validator<T> getDefaultValidator() {
  601. return new Validator<T>() {
  602. @Override
  603. public ValidationResult apply(T value, ValueContext context) {
  604. if (currentParseErrorMessage != null) {
  605. return ValidationResult.error(currentParseErrorMessage);
  606. }
  607. // Pass to range validator.
  608. return getRangeValidator().apply(value, context);
  609. }
  610. };
  611. }
  612. }