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

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