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.

BinderBookOfVaadinTest.java 31KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827
  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.data;
  17. import java.time.LocalDate;
  18. import java.util.List;
  19. import java.util.concurrent.atomic.AtomicBoolean;
  20. import java.util.concurrent.atomic.AtomicReference;
  21. import java.util.stream.Collectors;
  22. import org.junit.Assert;
  23. import org.junit.Before;
  24. import org.junit.Test;
  25. import com.vaadin.data.Binder.Binding;
  26. import com.vaadin.data.Binder.BindingBuilder;
  27. import com.vaadin.data.ValidationStatus.Status;
  28. import com.vaadin.data.util.converter.Converter;
  29. import com.vaadin.data.util.converter.StringToIntegerConverter;
  30. import com.vaadin.data.util.converter.ValueContext;
  31. import com.vaadin.data.validator.EmailValidator;
  32. import com.vaadin.data.validator.StringLengthValidator;
  33. import com.vaadin.server.AbstractErrorMessage;
  34. import com.vaadin.ui.Button;
  35. import com.vaadin.ui.DateField;
  36. import com.vaadin.ui.Label;
  37. import com.vaadin.ui.Notification;
  38. import com.vaadin.ui.Slider;
  39. import com.vaadin.ui.TextField;
  40. /**
  41. * Book of Vaadin tests.
  42. *
  43. * @author Vaadin Ltd
  44. *
  45. */
  46. public class BinderBookOfVaadinTest {
  47. private static class BookPerson {
  48. private String lastName;
  49. private String email, phone, title;
  50. private int yearOfBirth, salaryLevel;
  51. public BookPerson(int yearOfBirth, int salaryLevel) {
  52. this.yearOfBirth = yearOfBirth;
  53. this.salaryLevel = salaryLevel;
  54. }
  55. public BookPerson(BookPerson origin) {
  56. this(origin.yearOfBirth, origin.salaryLevel);
  57. lastName = origin.lastName;
  58. email = origin.email;
  59. phone = origin.phone;
  60. title = origin.title;
  61. }
  62. public String getLastName() {
  63. return lastName;
  64. }
  65. public void setLastName(String lastName) {
  66. this.lastName = lastName;
  67. }
  68. public int getYearOfBirth() {
  69. return yearOfBirth;
  70. }
  71. public void setYearOfBirth(int yearOfBirth) {
  72. this.yearOfBirth = yearOfBirth;
  73. }
  74. public int getSalaryLevel() {
  75. return salaryLevel;
  76. }
  77. public void setSalaryLevel(int salaryLevel) {
  78. this.salaryLevel = salaryLevel;
  79. }
  80. public String getEmail() {
  81. return email;
  82. }
  83. public void setEmail(String email) {
  84. this.email = email;
  85. }
  86. public String getPhone() {
  87. return phone;
  88. }
  89. public void setPhone(String phone) {
  90. this.phone = phone;
  91. }
  92. public String getTitle() {
  93. return title;
  94. }
  95. public void setTitle(String title) {
  96. this.title = title;
  97. }
  98. }
  99. public static class Trip {
  100. private LocalDate returnDate;
  101. public LocalDate getReturnDate() {
  102. return returnDate;
  103. }
  104. public void setReturnDate(LocalDate returnDate) {
  105. this.returnDate = returnDate;
  106. }
  107. }
  108. private Binder<BookPerson> binder;
  109. private TextField field;
  110. private TextField phoneField;
  111. private TextField emailField;
  112. @Before
  113. public void setUp() {
  114. binder = new Binder<>();
  115. field = new TextField();
  116. phoneField = new TextField();
  117. emailField = new TextField();
  118. }
  119. @Test
  120. public void loadingFromBusinessObjects() {
  121. // this test is just to make sure the code snippet in the book compiles
  122. binder.readBean(new BookPerson(1969, 50000));
  123. BinderValidationStatus<BookPerson> status = binder.validate();
  124. if (status.hasErrors()) {
  125. Notification.show("Validation error count: "
  126. + status.getValidationErrors().size());
  127. }
  128. }
  129. @Test
  130. public void handlingCheckedException() {
  131. // another test just to verify that book examples actually compile
  132. try {
  133. binder.writeBean(new BookPerson(2000, 50000));
  134. } catch (ValidationException e) {
  135. Notification.show("Validation error count: "
  136. + e.getValidationErrors().size());
  137. }
  138. }
  139. @Test
  140. public void simpleEmailValidator() {
  141. binder.forField(field)
  142. // Explicit validator instance
  143. .withValidator(new EmailValidator(
  144. "This doesn't look like a valid email address"))
  145. .bind(BookPerson::getEmail, BookPerson::setEmail);
  146. field.setValue("not-email");
  147. BinderValidationStatus<?> status = binder.validate();
  148. Assert.assertEquals(1, status.getFieldValidationErrors().size());
  149. Assert.assertEquals("This doesn't look like a valid email address",
  150. status.getFieldValidationErrors().get(0).getMessage().get());
  151. Assert.assertEquals("This doesn't look like a valid email address",
  152. ((AbstractErrorMessage) field.getErrorMessage()).getMessage());
  153. field.setValue("abc@vaadin.com");
  154. status = binder.validate();
  155. Assert.assertEquals(0, status.getBeanValidationErrors().size());
  156. Assert.assertNull(field.getErrorMessage());
  157. }
  158. @Test
  159. public void nameLengthTest() {
  160. binder.forField(field)
  161. // Validator defined based on a lambda and an error message
  162. .withValidator(name -> name.length() >= 3,
  163. "Last name must contain at least three characters")
  164. .bind(BookPerson::getLastName, BookPerson::setLastName);
  165. field.setValue("a");
  166. BinderValidationStatus<?> status = binder.validate();
  167. Assert.assertEquals(1, status.getFieldValidationErrors().size());
  168. Assert.assertEquals("Last name must contain at least three characters",
  169. status.getFieldValidationErrors().get(0).getMessage().get());
  170. Assert.assertEquals("Last name must contain at least three characters",
  171. ((AbstractErrorMessage) field.getErrorMessage()).getMessage());
  172. field.setValue("long last name");
  173. status = binder.validate();
  174. Assert.assertEquals(0, status.getFieldValidationErrors().size());
  175. Assert.assertNull(field.getErrorMessage());
  176. }
  177. @Test
  178. public void chainedEmailValidator() {
  179. binder.forField(field)
  180. // Explicit validator instance
  181. .withValidator(new EmailValidator(
  182. "This doesn't look like a valid email address"))
  183. .withValidator(email -> email.endsWith("@acme.com"),
  184. "Only acme.com email addresses are allowed")
  185. .bind(BookPerson::getEmail, BookPerson::setEmail);
  186. field.setValue("not-email");
  187. BinderValidationStatus<?> status = binder.validate();
  188. // Only one error per field should be reported
  189. Assert.assertEquals(1, status.getFieldValidationErrors().size());
  190. Assert.assertEquals("This doesn't look like a valid email address",
  191. status.getFieldValidationErrors().get(0).getMessage().get());
  192. Assert.assertEquals("This doesn't look like a valid email address",
  193. ((AbstractErrorMessage) field.getErrorMessage()).getMessage());
  194. field.setValue("abc@vaadin.com");
  195. status = binder.validate();
  196. Assert.assertEquals(1, status.getFieldValidationErrors().size());
  197. Assert.assertEquals("Only acme.com email addresses are allowed",
  198. status.getFieldValidationErrors().get(0).getMessage().get());
  199. Assert.assertEquals("Only acme.com email addresses are allowed",
  200. ((AbstractErrorMessage) field.getErrorMessage()).getMessage());
  201. field.setValue("abc@acme.com");
  202. status = binder.validate();
  203. Assert.assertEquals(0, status.getFieldValidationErrors().size());
  204. Assert.assertNull(field.getErrorMessage());
  205. }
  206. @Test
  207. public void converterBookOfVaadinExample1() {
  208. TextField yearOfBirthField = new TextField();
  209. // Slider for integers between 1 and 10
  210. Slider salaryLevelField = new Slider("Salary level", 1, 10);
  211. BindingBuilder<BookPerson, String> b1 = binder
  212. .forField(yearOfBirthField);
  213. BindingBuilder<BookPerson, Integer> b2 = b1.withConverter(
  214. new StringToIntegerConverter("Must enter a number"));
  215. b2.bind(BookPerson::getYearOfBirth, BookPerson::setYearOfBirth);
  216. BindingBuilder<BookPerson, Double> salaryBinding1 = binder
  217. .forField(salaryLevelField);
  218. BindingBuilder<BookPerson, Integer> salaryBinding2 = salaryBinding1
  219. .withConverter(Double::intValue, Integer::doubleValue);
  220. salaryBinding2.bind(BookPerson::getSalaryLevel,
  221. BookPerson::setSalaryLevel);
  222. // Test that the book code works
  223. BookPerson bookPerson = new BookPerson(1972, 4);
  224. binder.setBean(bookPerson);
  225. Assert.assertEquals(4.0, salaryLevelField.getValue().doubleValue(), 0);
  226. Assert.assertEquals("1,972", yearOfBirthField.getValue());
  227. bookPerson.setSalaryLevel(8);
  228. binder.readBean(bookPerson);
  229. Assert.assertEquals(8.0, salaryLevelField.getValue().doubleValue(), 0);
  230. bookPerson.setYearOfBirth(123);
  231. binder.readBean(bookPerson);
  232. Assert.assertEquals("123", yearOfBirthField.getValue());
  233. yearOfBirthField.setValue("2016");
  234. salaryLevelField.setValue(1.0);
  235. Assert.assertEquals(2016, bookPerson.getYearOfBirth());
  236. Assert.assertEquals(1, bookPerson.getSalaryLevel());
  237. }
  238. @Test
  239. public void converterBookOfVaadinExample2() {
  240. TextField yearOfBirthField = new TextField();
  241. binder.forField(yearOfBirthField)
  242. .withConverter(Integer::valueOf, String::valueOf,
  243. // Text to use instead of the NumberFormatException
  244. // message
  245. "Please enter a number")
  246. .bind(BookPerson::getYearOfBirth, BookPerson::setYearOfBirth);
  247. binder.setBean(new BookPerson(1900, 5));
  248. yearOfBirthField.setValue("abc");
  249. binder.validate();
  250. Assert.assertEquals("Please&#32;enter&#32;a&#32;number",
  251. yearOfBirthField.getComponentError().getFormattedHtmlMessage());
  252. }
  253. @Test
  254. public void crossFieldValidation_validateUsingBinder() {
  255. Binder<Trip> binder = new Binder<>();
  256. DateField departing = new DateField("Departing");
  257. DateField returning = new DateField("Returning");
  258. Binding<Trip, LocalDate> returnBinding = binder.forField(returning)
  259. .withValidator(
  260. returnDate -> !returnDate
  261. .isBefore(departing.getValue()),
  262. "Cannot return before departing")
  263. .bind(Trip::getReturnDate, Trip::setReturnDate);
  264. departing.addValueChangeListener(event -> returnBinding.validate());
  265. LocalDate past = LocalDate.now();
  266. LocalDate before = past.plusDays(1);
  267. LocalDate after = before.plusDays(1);
  268. departing.setValue(before);
  269. returning.setValue(after);
  270. BinderValidationStatus<Trip> status = binder.validate();
  271. Assert.assertTrue(status.getBeanValidationErrors().isEmpty());
  272. Assert.assertNull(departing.getComponentError());
  273. Assert.assertNull(returning.getComponentError());
  274. // update returning => validation is done against this field
  275. returning.setValue(past);
  276. status = binder.validate();
  277. Assert.assertFalse(status.getFieldValidationErrors().isEmpty());
  278. Assert.assertNotNull(returning.getComponentError());
  279. Assert.assertNull(departing.getComponentError());
  280. // set correct value back
  281. returning.setValue(before);
  282. status = binder.validate();
  283. Assert.assertTrue(status.getFieldValidationErrors().isEmpty());
  284. Assert.assertNull(departing.getComponentError());
  285. Assert.assertNull(returning.getComponentError());
  286. // update departing => validation is done because of listener added
  287. departing.setValue(after);
  288. status = binder.validate();
  289. Assert.assertFalse(status.getFieldValidationErrors().isEmpty());
  290. Assert.assertNotNull(returning.getComponentError());
  291. Assert.assertNull(departing.getComponentError());
  292. }
  293. @Test
  294. public void crossFieldValidation_validateUsingBinding() {
  295. Binder<Trip> binder = new Binder<>();
  296. DateField departing = new DateField("Departing");
  297. DateField returning = new DateField("Returning");
  298. Binding<Trip, LocalDate> returnBinding = binder.forField(returning)
  299. .withValidator(
  300. returnDate -> !returnDate
  301. .isBefore(departing.getValue()),
  302. "Cannot return before departing")
  303. .bind(Trip::getReturnDate, Trip::setReturnDate);
  304. departing.addValueChangeListener(event -> returnBinding.validate());
  305. LocalDate past = LocalDate.now();
  306. LocalDate before = past.plusDays(1);
  307. LocalDate after = before.plusDays(1);
  308. departing.setValue(before);
  309. returning.setValue(after);
  310. ValidationStatus<LocalDate> result = returnBinding.validate();
  311. Assert.assertFalse(result.isError());
  312. Assert.assertNull(departing.getComponentError());
  313. // update returning => validation is done against this field
  314. returning.setValue(past);
  315. result = returnBinding.validate();
  316. Assert.assertTrue(result.isError());
  317. Assert.assertNotNull(returning.getComponentError());
  318. // set correct value back
  319. returning.setValue(before);
  320. result = returnBinding.validate();
  321. Assert.assertFalse(result.isError());
  322. Assert.assertNull(departing.getComponentError());
  323. // update departing => validation is done because of listener added
  324. departing.setValue(after);
  325. result = returnBinding.validate();
  326. Assert.assertTrue(result.isError());
  327. Assert.assertNotNull(returning.getComponentError());
  328. }
  329. @Test
  330. public void withStatusLabelExample() {
  331. Label emailStatus = new Label();
  332. String msg = "This doesn't look like a valid email address";
  333. binder.forField(field).withValidator(new EmailValidator(msg))
  334. .withStatusLabel(emailStatus)
  335. .bind(BookPerson::getEmail, BookPerson::setEmail);
  336. field.setValue("foo");
  337. binder.validate();
  338. Assert.assertTrue(emailStatus.isVisible());
  339. Assert.assertEquals(msg, emailStatus.getValue());
  340. field.setValue("foo@vaadin.com");
  341. binder.validate();
  342. Assert.assertFalse(emailStatus.isVisible());
  343. Assert.assertEquals("", emailStatus.getValue());
  344. }
  345. @Test
  346. public void withBindingStatusHandlerExample() {
  347. Label nameStatus = new Label();
  348. AtomicReference<ValidationStatus<?>> statusCapture = new AtomicReference<>();
  349. String msg = "Full name must contain at least three characters";
  350. binder.forField(field).withValidator(name -> name.length() >= 3, msg)
  351. .withValidationStatusHandler(status -> {
  352. nameStatus.setValue(status.getMessage().orElse(""));
  353. // Only show the label when validation has failed
  354. boolean error = status.getStatus() == Status.ERROR;
  355. nameStatus.setVisible(error);
  356. statusCapture.set(status);
  357. }).bind(BookPerson::getLastName, BookPerson::setLastName);
  358. field.setValue("aa");
  359. binder.validate();
  360. Assert.assertTrue(nameStatus.isVisible());
  361. Assert.assertEquals(msg, nameStatus.getValue());
  362. Assert.assertNotNull(statusCapture.get());
  363. ValidationStatus<?> status = statusCapture.get();
  364. Assert.assertEquals(Status.ERROR, status.getStatus());
  365. Assert.assertEquals(msg, status.getMessage().get());
  366. Assert.assertEquals(field, status.getField());
  367. field.setValue("foo");
  368. binder.validate();
  369. Assert.assertFalse(nameStatus.isVisible());
  370. Assert.assertEquals("", nameStatus.getValue());
  371. Assert.assertNotNull(statusCapture.get());
  372. status = statusCapture.get();
  373. Assert.assertEquals(Status.OK, status.getStatus());
  374. Assert.assertFalse(status.getMessage().isPresent());
  375. Assert.assertEquals(field, status.getField());
  376. }
  377. @Test
  378. public void binder_saveIfValid() {
  379. BeanBinder<BookPerson> binder = new BeanBinder<>(BookPerson.class);
  380. // Phone or email has to be specified for the bean
  381. Validator<BookPerson> phoneOrEmail = Validator.from(
  382. personBean -> !"".equals(personBean.getPhone())
  383. || !"".equals(personBean.getEmail()),
  384. "A person must have either a phone number or an email address");
  385. binder.withValidator(phoneOrEmail);
  386. binder.forField(emailField).bind("email");
  387. binder.forField(phoneField).bind("phone");
  388. // Person person = // e.g. JPA entity or bean from Grid
  389. BookPerson person = new BookPerson(1900, 5);
  390. person.setEmail("Old Email");
  391. // Load person data to a form
  392. binder.readBean(person);
  393. Button saveButton = new Button("Save", event -> {
  394. // Using saveIfValid to avoid the try-catch block that is
  395. // needed if using the regular save method
  396. if (binder.writeBeanIfValid(person)) {
  397. // Person is valid and updated
  398. // TODO Store in the database
  399. }
  400. });
  401. emailField.setValue("foo@bar.com");
  402. Assert.assertTrue(binder.writeBeanIfValid(person));
  403. // Person updated
  404. Assert.assertEquals("foo@bar.com", person.getEmail());
  405. emailField.setValue("");
  406. Assert.assertFalse(binder.writeBeanIfValid(person));
  407. // Person updated because phone and email are both empty
  408. Assert.assertEquals("foo@bar.com", person.getEmail());
  409. }
  410. @Test
  411. public void manyConvertersAndValidators() throws ValidationException {
  412. TextField yearOfBirthField = new TextField();
  413. binder.forField(yearOfBirthField)
  414. // Validator will be run with the String value of the field
  415. .withValidator(text -> text.length() == 4,
  416. "Doesn't look like a year")
  417. // Converter will only be run for strings with 4 characters
  418. .withConverter(
  419. new StringToIntegerConverter("Must enter a number"))
  420. // Validator will be run with the converted value
  421. .withValidator(year -> year >= 1900 && year <= 2000,
  422. "Person must be born in the 20th century")
  423. .bind(BookPerson::getYearOfBirth, BookPerson::setYearOfBirth);
  424. yearOfBirthField.setValue("abc");
  425. Assert.assertEquals("Doesn't look like a year", binder.validate()
  426. .getFieldValidationErrors().get(0).getMessage().get());
  427. yearOfBirthField.setValue("abcd");
  428. Assert.assertEquals("Must enter a number", binder.validate()
  429. .getFieldValidationErrors().get(0).getMessage().get());
  430. yearOfBirthField.setValue("1200");
  431. Assert.assertEquals("Person must be born in the 20th century",
  432. binder.validate().getFieldValidationErrors().get(0).getMessage()
  433. .get());
  434. yearOfBirthField.setValue("1950");
  435. Assert.assertFalse(binder.validate().hasErrors());
  436. BookPerson person = new BookPerson(1500, 12);
  437. binder.writeBean(person);
  438. Assert.assertEquals(1950, person.getYearOfBirth());
  439. }
  440. class MyConverter implements Converter<String, Integer> {
  441. @Override
  442. public Result<Integer> convertToModel(String fieldValue,
  443. ValueContext context) {
  444. // Produces a converted value or an error
  445. try {
  446. // ok is a static helper method that creates a Result
  447. return Result.ok(Integer.valueOf(fieldValue));
  448. } catch (NumberFormatException e) {
  449. // error is a static helper method that creates a Result
  450. return Result.error("Please enter a number");
  451. }
  452. }
  453. @Override
  454. public String convertToPresentation(Integer integer,
  455. ValueContext context) {
  456. // Converting to the field type should always succeed,
  457. // so there is no support for returning an error Result.
  458. return String.valueOf(integer);
  459. }
  460. }
  461. @Test
  462. public void bindUsingCustomConverter() {
  463. Binder<BookPerson> binder = new Binder<>();
  464. TextField yearOfBirthField = new TextField();
  465. // Using the converter
  466. binder.forField(yearOfBirthField).withConverter(new MyConverter())
  467. .bind(BookPerson::getYearOfBirth, BookPerson::setYearOfBirth);
  468. BookPerson p = new BookPerson(1500, 12);
  469. binder.setBean(p);
  470. yearOfBirthField.setValue("abc");
  471. Assert.assertTrue(binder.validate().hasErrors());
  472. Assert.assertEquals("Please enter a number", binder.validate()
  473. .getFieldValidationErrors().get(0).getMessage().get());
  474. yearOfBirthField.setValue("123");
  475. Assert.assertTrue(binder.validate().isOk());
  476. p.setYearOfBirth(12500);
  477. binder.readBean(p);
  478. Assert.assertEquals("12500", yearOfBirthField.getValue());
  479. Assert.assertTrue(binder.validate().isOk());
  480. }
  481. @Test
  482. public void withBinderStatusLabelExample() {
  483. Label formStatusLabel = new Label();
  484. BeanBinder<BookPerson> binder = new BeanBinder<>(BookPerson.class);
  485. binder.setStatusLabel(formStatusLabel);
  486. final String message = "Too young, son";
  487. final String message2 = "Y2K error";
  488. TextField yearOfBirth = new TextField();
  489. BookPerson p = new BookPerson(1500, 12);
  490. binder.forField(yearOfBirth)
  491. .withConverter(new StringToIntegerConverter("err"))
  492. .bind(BookPerson::getYearOfBirth, BookPerson::setYearOfBirth);
  493. binder.withValidator(bean -> bean.yearOfBirth < 2000, message)
  494. .withValidator(bean -> bean.yearOfBirth != 2000, message2);
  495. binder.setBean(p);
  496. // first bean validator fails and passes error message to status label
  497. yearOfBirth.setValue("2001");
  498. BinderValidationStatus<?> status = binder.validate();
  499. Assert.assertEquals(0, status.getFieldValidationErrors().size());
  500. Assert.assertEquals(1, status.getBeanValidationErrors().size());
  501. Assert.assertEquals(message,
  502. status.getBeanValidationErrors().get(0).getErrorMessage());
  503. Assert.assertEquals(message, formStatusLabel.getValue());
  504. // value is correct, status label is cleared
  505. yearOfBirth.setValue("1999");
  506. status = binder.validate();
  507. Assert.assertFalse(status.hasErrors());
  508. Assert.assertEquals("", formStatusLabel.getValue());
  509. // both bean validators fail, should be two error messages chained
  510. yearOfBirth.setValue("2000");
  511. status = binder.validate();
  512. Assert.assertEquals(2, status.getBeanValidationResults().size());
  513. Assert.assertEquals(0, status.getFieldValidationErrors().size());
  514. Assert.assertEquals(2, status.getBeanValidationErrors().size());
  515. // only first error is shown
  516. Assert.assertEquals(message, formStatusLabel.getValue());
  517. }
  518. @Test
  519. public void withBinderStatusHandlerExample() {
  520. Label formStatusLabel = new Label();
  521. BinderValidationStatusHandler<BookPerson> defaultHandler = binder
  522. .getValidationStatusHandler();
  523. binder.setValidationStatusHandler(status -> {
  524. // create an error message on failed bean level validations
  525. List<ValidationResult> errors = status.getBeanValidationErrors();
  526. String errorMessage = errors.stream()
  527. .map(ValidationResult::getErrorMessage)
  528. .collect(Collectors.joining("\n"));
  529. // show error in a label
  530. formStatusLabel.setValue(errorMessage);
  531. formStatusLabel.setVisible(!errorMessage.isEmpty());
  532. // Let the default handler show messages for each field
  533. defaultHandler.accept(status);
  534. });
  535. final String bindingMessage = "uneven";
  536. final String message = "Too young, son";
  537. final String message2 = "Y2K error";
  538. TextField yearOfBirth = new TextField();
  539. BookPerson p = new BookPerson(1500, 12);
  540. binder.forField(yearOfBirth)
  541. .withConverter(new StringToIntegerConverter("err"))
  542. .withValidator(value -> value % 2 == 0, bindingMessage)
  543. .bind(BookPerson::getYearOfBirth, BookPerson::setYearOfBirth);
  544. binder.withValidator(bean -> bean.yearOfBirth < 2000, message)
  545. .withValidator(bean -> bean.yearOfBirth != 2000, message2);
  546. binder.setBean(p);
  547. // first binding validation fails, no bean level validation is done
  548. yearOfBirth.setValue("2001");
  549. BinderValidationStatus<?> status = binder.validate();
  550. Assert.assertEquals(1, status.getFieldValidationErrors().size());
  551. Assert.assertEquals(bindingMessage,
  552. status.getFieldValidationErrors().get(0).getMessage().get());
  553. Assert.assertEquals("", formStatusLabel.getValue());
  554. // first bean validator fails and passes error message to status label
  555. yearOfBirth.setValue("2002");
  556. status = binder.validate();
  557. Assert.assertEquals(0, status.getFieldValidationErrors().size());
  558. Assert.assertEquals(1, status.getBeanValidationErrors().size());
  559. Assert.assertEquals(message,
  560. status.getBeanValidationErrors().get(0).getErrorMessage());
  561. Assert.assertEquals(message, formStatusLabel.getValue());
  562. // value is correct, status label is cleared
  563. yearOfBirth.setValue("1998");
  564. status = binder.validate();
  565. Assert.assertTrue(status.isOk());
  566. Assert.assertFalse(status.hasErrors());
  567. Assert.assertEquals(0, status.getFieldValidationErrors().size());
  568. Assert.assertEquals(0, status.getBeanValidationErrors().size());
  569. Assert.assertEquals("", formStatusLabel.getValue());
  570. // both bean validators fail, should be two error messages chained
  571. yearOfBirth.setValue("2000");
  572. status = binder.validate();
  573. Assert.assertEquals(0, status.getFieldValidationErrors().size());
  574. Assert.assertEquals(2, status.getBeanValidationErrors().size());
  575. Assert.assertEquals(message + "\n" + message2,
  576. formStatusLabel.getValue());
  577. }
  578. @Test
  579. public void statusChangeListener_binderIsNotBound() {
  580. Button saveButton = new Button();
  581. Button resetButton = new Button();
  582. AtomicBoolean eventIsFired = new AtomicBoolean(false);
  583. binder.addStatusChangeListener(event -> {
  584. boolean isValid = !event.hasValidationErrors();
  585. boolean hasChanges = event.getBinder().hasChanges();
  586. eventIsFired.set(true);
  587. saveButton.setEnabled(hasChanges && isValid);
  588. resetButton.setEnabled(hasChanges);
  589. });
  590. binder.forField(field)
  591. .withValidator(new StringLengthValidator("", 1, 3))
  592. .bind(BookPerson::getLastName, BookPerson::setLastName);
  593. // no changes
  594. Assert.assertFalse(saveButton.isEnabled());
  595. Assert.assertFalse(resetButton.isEnabled());
  596. verifyEventIsFired(eventIsFired);
  597. BookPerson person = new BookPerson(2000, 1);
  598. binder.readBean(person);
  599. // no changes
  600. Assert.assertFalse(saveButton.isEnabled());
  601. Assert.assertFalse(resetButton.isEnabled());
  602. verifyEventIsFired(eventIsFired);
  603. field.setValue("a");
  604. // binder is not bound, no event fired
  605. // no changes: see #375. There should be a change and enabled state
  606. Assert.assertTrue(saveButton.isEnabled());
  607. Assert.assertTrue(resetButton.isEnabled());
  608. Assert.assertTrue(eventIsFired.get());
  609. binder.writeBeanIfValid(person);
  610. // no changes
  611. Assert.assertFalse(saveButton.isEnabled());
  612. Assert.assertFalse(resetButton.isEnabled());
  613. verifyEventIsFired(eventIsFired);
  614. binder.validate();
  615. // no changes
  616. Assert.assertFalse(saveButton.isEnabled());
  617. Assert.assertFalse(resetButton.isEnabled());
  618. verifyEventIsFired(eventIsFired);
  619. field.setValue("");
  620. // binder is not bound, no event fired
  621. // no changes: see #375. There should be a change and disabled state for
  622. // save button because of failed validation
  623. Assert.assertFalse(saveButton.isEnabled());
  624. Assert.assertTrue(resetButton.isEnabled());
  625. Assert.assertTrue(eventIsFired.get());
  626. }
  627. @Test
  628. public void statusChangeListener_binderIsBound() {
  629. Button saveButton = new Button();
  630. Button resetButton = new Button();
  631. AtomicBoolean eventIsFired = new AtomicBoolean(false);
  632. binder.addStatusChangeListener(event -> {
  633. boolean isValid = !event.hasValidationErrors();
  634. boolean hasChanges = event.getBinder().hasChanges();
  635. eventIsFired.set(true);
  636. saveButton.setEnabled(hasChanges && isValid);
  637. resetButton.setEnabled(hasChanges);
  638. });
  639. binder.forField(field)
  640. .withValidator(new StringLengthValidator("", 1, 3))
  641. .bind(BookPerson::getLastName, BookPerson::setLastName);
  642. // no changes
  643. Assert.assertFalse(saveButton.isEnabled());
  644. Assert.assertFalse(resetButton.isEnabled());
  645. verifyEventIsFired(eventIsFired);
  646. BookPerson person = new BookPerson(2000, 1);
  647. binder.setBean(person);
  648. // no changes
  649. Assert.assertFalse(saveButton.isEnabled());
  650. Assert.assertFalse(resetButton.isEnabled());
  651. verifyEventIsFired(eventIsFired);
  652. field.setValue("a");
  653. // there are valid changes
  654. verifyEventIsFired(eventIsFired);
  655. field.setValue("");
  656. // there are invalid changes
  657. Assert.assertFalse(saveButton.isEnabled());
  658. Assert.assertTrue(resetButton.isEnabled());
  659. verifyEventIsFired(eventIsFired);
  660. // set valid value
  661. field.setValue("a");
  662. verifyEventIsFired(eventIsFired);
  663. binder.writeBeanIfValid(person);
  664. // there are no changes.
  665. Assert.assertFalse(saveButton.isEnabled());
  666. Assert.assertFalse(resetButton.isEnabled());
  667. verifyEventIsFired(eventIsFired);
  668. }
  669. private void verifyEventIsFired(AtomicBoolean flag) {
  670. Assert.assertTrue(flag.get());
  671. flag.set(false);
  672. }
  673. }