From 28f5c845793be9a0db3f2acef914a68ccc91ce80 Mon Sep 17 00:00:00 2001 From: =?utf8?q?Pekka=20Hyv=C3=B6nen?= Date: Mon, 12 Sep 2016 12:05:19 +0300 Subject: [PATCH] Refactor Binder Status Handling API BinderStatusHandler is now triggered only once per validation. Unified ValidationError and BinderResult into BinderValidationStatus. Renamed ValidationStatusChangeEvent into ValidationStatus. Unified handler names for validation status. Next patch will fix resetting of field errors on reset. Change-Id: I9536d554d781fe599fbd7e5bcb5a9ffebe675ca0 --- .../datamodel/datamodel-forms.asciidoc | 34 +- .../main/java/com/vaadin/data/BeanBinder.java | 4 +- .../src/main/java/com/vaadin/data/Binder.java | 269 ++++++++------- .../java/com/vaadin/data/BinderResult.java | 72 ----- .../com/vaadin/data/BinderStatusHandler.java | 5 +- .../vaadin/data/BinderValidationStatus.java | 161 +++++++++ .../data/BinderValidationStatusHandler.java | 50 +++ .../java/com/vaadin/data/ValidationError.java | 104 ------ .../com/vaadin/data/ValidationException.java | 60 +++- .../com/vaadin/data/ValidationStatus.java | 146 ++++++++- .../data/ValidationStatusChangeEvent.java | 93 ------ ...dler.java => ValidationStatusHandler.java} | 21 +- .../java/com/vaadin/data/BeanBinderTest.java | 7 +- .../vaadin/data/BinderBookOfVaadinTest.java | 194 ++++++----- .../test/java/com/vaadin/data/BinderTest.java | 306 ++++++++++++------ 15 files changed, 886 insertions(+), 640 deletions(-) delete mode 100644 server/src/main/java/com/vaadin/data/BinderResult.java create mode 100644 server/src/main/java/com/vaadin/data/BinderValidationStatus.java create mode 100644 server/src/main/java/com/vaadin/data/BinderValidationStatusHandler.java delete mode 100644 server/src/main/java/com/vaadin/data/ValidationError.java delete mode 100644 server/src/main/java/com/vaadin/data/ValidationStatusChangeEvent.java rename server/src/main/java/com/vaadin/data/{StatusChangeHandler.java => ValidationStatusHandler.java} (58%) diff --git a/documentation/datamodel/datamodel-forms.asciidoc b/documentation/datamodel/datamodel-forms.asciidoc index 62fd9df38c..62d844dee6 100644 --- a/documentation/datamodel/datamodel-forms.asciidoc +++ b/documentation/datamodel/datamodel-forms.asciidoc @@ -323,11 +323,11 @@ Even if the user has not edited a field, all validation error will be shown if w binder.load(new Person()); // This will make all current validation errors visible -List> validationErrors = binder.validate(); +ValidationStatus status = binder.validate(); -if (!validationErrors.isEmpty()) { +if (status.hasErrors()) { Notification.show("Validation error count: " - + validationErrors.size()); + + status.getValidationErrors().size()); } ---- @@ -341,7 +341,7 @@ Handling a checked exception:: ---- try { binder.save(person); -} catch (BindingException e) { +} catch (ValidationException e) { Notification.show("Validation error count: " + e.getValidationErrors().size()); } @@ -566,21 +566,17 @@ We can also define our own status handler to provide a custom way of handling st ---- BinderStatusHandler defaultHandler = binder.getStatusHandler(); -binder.setStatusHandler(results -> { - String errorMessage = results.stream() - // Ignore helper and confirmation messages - .filter(BinderResult::isError) - // Ignore messages that belong to a specific field - .filter(error -> !error.getField().isPresent()) - // Create a string out of the remaining messages - .map(Result::getMessage).map(o -> o.get()) - .collect(Collectors.joining("\n")); - - formStatusLabel.setValue(errorMessage); - formStatusLabel.setVisible(!errorMessage.isEmpty()); - - // Let the default handler show messages for each field - defaultHandler.accept(event); +binder.setStatusHandler(status -> { + // create an error message on failed bean level validations + List> errors = status.getBeanValidationErrors(); + String errorMessage = errors.stream().map(Result::getMessage) + .map(o -> o.get()).collect(Collectors.joining("\n")); + // show error in a label + formStatusLabel.setValue(errorMessage); + formStatusLabel.setVisible(!errorMessage.isEmpty()); + + // Let the default handler show messages for each field + defaultHandler.accept(status); }); ---- diff --git a/server/src/main/java/com/vaadin/data/BeanBinder.java b/server/src/main/java/com/vaadin/data/BeanBinder.java index ff7e329660..cbabdb80cb 100644 --- a/server/src/main/java/com/vaadin/data/BeanBinder.java +++ b/server/src/main/java/com/vaadin/data/BeanBinder.java @@ -149,7 +149,7 @@ public class BeanBinder extends Binder { protected BeanBindingImpl(BeanBinder binder, HasValue field, Converter converter, - StatusChangeHandler statusChangeHandler) { + ValidationStatusHandler statusChangeHandler) { super(binder, field, converter, statusChangeHandler); } @@ -318,7 +318,7 @@ public class BeanBinder extends Binder { @Override protected BeanBindingImpl createBinding( HasValue field, Converter converter, - StatusChangeHandler handler) { + ValidationStatusHandler handler) { Objects.requireNonNull(field, "field cannot be null"); Objects.requireNonNull(converter, "converter cannot be null"); return new BeanBindingImpl<>(this, field, converter, handler); diff --git a/server/src/main/java/com/vaadin/data/Binder.java b/server/src/main/java/com/vaadin/data/Binder.java index af5f429252..c282a89e77 100644 --- a/server/src/main/java/com/vaadin/data/Binder.java +++ b/server/src/main/java/com/vaadin/data/Binder.java @@ -300,15 +300,15 @@ public class Binder implements Serializable { * default behavior). *

* This is just a shorthand for - * {@link #withStatusChangeHandler(StatusChangeHandler)} method where - * the handler instance hides the {@code label} if there is no error and + * {@link #withStatusHandler(StatusChangeHandler)} method where the + * handler instance hides the {@code label} if there is no error and * shows it with validation error message if validation fails. It means * that it cannot be called after - * {@link #withStatusChangeHandler(StatusChangeHandler)} method call or - * {@link #withStatusChangeHandler(StatusChangeHandler)} after this - * method call. + * {@link #withStatusHandler(StatusChangeHandler)} method call or + * {@link #withStatusHandler(StatusChangeHandler)} after this method + * call. * - * @see #withStatusChangeHandler(StatusChangeHandler) + * @see #withStatusHandler(StatusChangeHandler) * @see AbstractComponent#setComponentError(ErrorMessage) * @param label * label to show validation status for the field @@ -316,11 +316,10 @@ public class Binder implements Serializable { */ public default Binding withStatusLabel( Label label) { - return withStatusChangeHandler(event -> { - label.setValue(event.getMessage().orElse("")); + return withStatusHandler(status -> { + label.setValue(status.getMessage().orElse("")); // Only show the label when validation has failed - label.setVisible( - ValidationStatus.ERROR.equals(event.getStatus())); + label.setVisible(status.isError()); }); } @@ -353,19 +352,19 @@ public class Binder implements Serializable { * status change handler * @return this binding, for chaining */ - public Binding withStatusChangeHandler( - StatusChangeHandler handler); + public Binding withStatusHandler( + ValidationStatusHandler handler); /** - * Validates the field value and returns a {@code Result} instance - * representing the outcome of the validation. + * Validates the field value and returns a {@code ValidationStatus} + * instance representing the outcome of the validation. * * @see Binder#validate() * @see Validator#apply(Object) * * @return the validation result. */ - public Result validate(); + public ValidationStatus validate(); } @@ -387,7 +386,7 @@ public class Binder implements Serializable { private final HasValue field; private Registration onValueChange; - private StatusChangeHandler statusChangeHandler; + private ValidationStatusHandler statusHandler; private boolean isStatusHandlerChanged; private Function getter; @@ -414,11 +413,11 @@ public class Binder implements Serializable { */ protected BindingImpl(Binder binder, HasValue field, Converter converterValidatorChain, - StatusChangeHandler statusChangeHandler) { + ValidationStatusHandler statusChangeHandler) { this.field = field; this.binder = binder; this.converterValidatorChain = converterValidatorChain; - this.statusChangeHandler = statusChangeHandler; + this.statusHandler = statusChangeHandler; } @Override @@ -451,13 +450,12 @@ public class Binder implements Serializable { Objects.requireNonNull(converter, "converter cannot be null"); return getBinder().createBinding(getField(), - converterValidatorChain.chain(converter), - statusChangeHandler); + converterValidatorChain.chain(converter), statusHandler); } @Override - public Binding withStatusChangeHandler( - StatusChangeHandler handler) { + public Binding withStatusHandler( + ValidationStatusHandler handler) { checkUnbound(); Objects.requireNonNull(handler, "handler cannot be null"); if (isStatusHandlerChanged) { @@ -465,7 +463,7 @@ public class Binder implements Serializable { "A StatusChangeHandler has already been set"); } isStatusHandlerChanged = true; - statusChangeHandler = handler; + statusHandler = handler; return this; } @@ -500,33 +498,31 @@ public class Binder implements Serializable { private void bind(BEAN bean) { setFieldValue(bean); - onValueChange = getField().addValueChangeListener(e -> { - binder.setHasChanges(true); - storeFieldValue(bean, true); - }); + onValueChange = getField() + .addValueChangeListener(e -> handleFieldValueChange(bean)); } @Override - public Result validate() { - BinderResult bindingResult = getTargetValue(); - getBinder().getStatusHandler().accept(Arrays.asList(bindingResult)); - return bindingResult; + public ValidationStatus validate() { + ValidationStatus status = getTargetValue(); + getBinder().getStatusHandler() + .accept(new BinderValidationStatus<>(getBinder(), + Arrays.asList(status), Collections.emptyList())); + return status; } /** * Returns the field value run through all converters and validators, - * but doesn't fire a {@link ValidationStatusChangeEvent status change - * event}. + * but doesn't pass the {@link ValidationStatus} to any status handler. * * @return a result containing the validated and converted value or * describing an error */ - private BinderResult getTargetValue() { + private ValidationStatus getTargetValue() { FIELDVALUE fieldValue = field.getValue(); Result dataValue = converterValidatorChain.convertToModel( fieldValue, ((AbstractComponent) field).getLocale()); - return dataValue.biMap((value, message) -> new BinderResult<>(this, - value, message)); + return new ValidationStatus<>(this, dataValue); } private void unbind() { @@ -551,45 +547,55 @@ public class Binder implements Serializable { ((AbstractComponent) getField()).getLocale()); } + /** + * Handles the value change triggered by the bound field. + * + * @param bean + * the new value + */ + private void handleFieldValueChange(BEAN bean) { + binder.setHasChanges(true); + // store field value if valid + ValidationStatus fieldValidationStatus = storeFieldValue( + bean); + List> binderValidationResults; + // if all field level validations pass, run bean level validation + if (!getBinder().bindings.stream().map(BindingImpl::getTargetValue) + .anyMatch(ValidationStatus::isError)) { + binderValidationResults = binder.validateItem(bean); + } else { + binderValidationResults = Collections.emptyList(); + } + binder.getStatusHandler() + .accept(new BinderValidationStatus<>(binder, + Arrays.asList(fieldValidationStatus), + binderValidationResults)); + } + /** * Saves the field value by invoking the setter function on the given - * bean, if the value passes all registered validators. Optionally runs - * item level validators if all field validators pass. + * bean, if the value passes all registered validators. * * @param bean * the bean to set the property value to - * @param runBeanLevelValidation - * true to run item level validators if all - * field validators pass, false to always skip - * item level validators */ - private void storeFieldValue(BEAN bean, - boolean runBeanLevelValidation) { + private ValidationStatus storeFieldValue(BEAN bean) { assert bean != null; + + ValidationStatus validationStatus = getTargetValue(); if (setter != null) { - BinderResult validationResult = getTargetValue(); - getBinder().getStatusHandler() - .accept(Arrays.asList(validationResult)); - validationResult.ifOk(value -> setter.accept(bean, value)); - } - if (runBeanLevelValidation && !getBinder().bindings.stream() - .map(BindingImpl::getTargetValue) - .anyMatch(Result::isError)) { - binder.validateItem(bean); + validationStatus.getResult().ifPresent(result -> result + .ifOk(value -> setter.accept(bean, value))); } + return validationStatus; } private void setBeanValue(BEAN bean, TARGET value) { setter.accept(bean, value); } - private void fireStatusChangeEvent(Result result) { - ValidationStatusChangeEvent event = new ValidationStatusChangeEvent( - getField(), - result.isError() ? ValidationStatus.ERROR - : ValidationStatus.OK, - result.getMessage().orElse(null)); - statusChangeHandler.accept(event); + private void notifyStatusChangeHandler(ValidationStatus status) { + statusHandler.accept(status); } } @@ -806,9 +812,10 @@ public class Binder implements Serializable { * if some of the bound field values fail to validate */ public void save(BEAN bean) throws ValidationException { - List> errors = doSaveIfValid(bean); - if (!errors.isEmpty()) { - throw new ValidationException(errors); + BinderValidationStatus status = doSaveIfValid(bean); + if (status.hasErrors()) { + throw new ValidationException(status.getFieldValidationErrors(), + status.getBeanValidationErrors()); } } @@ -833,7 +840,7 @@ public class Binder implements Serializable { * updated, {@code false} otherwise */ public boolean saveIfValid(BEAN bean) { - return doSaveIfValid(bean).isEmpty(); + return doSaveIfValid(bean).isOk(); } /** @@ -845,13 +852,15 @@ public class Binder implements Serializable { * @return a list of field validation errors if such occur, otherwise a list * of bean validation errors. */ - private List> doSaveIfValid(BEAN bean) { + private BinderValidationStatus doSaveIfValid(BEAN bean) { Objects.requireNonNull(bean, "bean cannot be null"); // First run fields level validation - List> errors = validateBindings(); + List> bindingStatuses = validateBindings(); // If no validation errors then update bean - if (!errors.isEmpty()) { - return errors; + if (bindingStatuses.stream().filter(ValidationStatus::isError).findAny() + .isPresent()) { + return new BinderValidationStatus<>(this, bindingStatuses, + Collections.emptyList()); } // Save old bean values so we can restore them if validators fail @@ -859,10 +868,11 @@ public class Binder implements Serializable { bindings.forEach(binding -> oldValues.put(binding, binding.convertDataToFieldType(bean))); - bindings.forEach(binding -> binding.storeFieldValue(bean, false)); + bindings.forEach(binding -> binding.storeFieldValue(bean)); // Now run bean level validation against the updated bean - List> itemValidatorErrors = validateItem(bean); - if (!itemValidatorErrors.isEmpty()) { + List> binderResults = validateItem(bean); + if (binderResults.stream().filter(Result::isError).findAny() + .isPresent()) { // Item validator failed, revert values bindings.forEach((BindingImpl binding) -> binding.setBeanValue(bean, oldValues.get(binding))); @@ -870,7 +880,8 @@ public class Binder implements Serializable { // Save successful, reset hasChanges to false setHasChanges(false); } - return itemValidatorErrors; + return new BinderValidationStatus<>(this, bindingStatuses, + binderResults); } /** @@ -894,68 +905,56 @@ public class Binder implements Serializable { } /** - * Validates the values of all bound fields and returns the result of the - * validation as a list of validation errors. + * Validates the values of all bound fields and returns the validation + * status. *

* If all field level validators pass, and {@link #bind(Object)} has been * used to bind to an item, item level validators are run for that bean. * Item level validators are ignored if there is no bound item or if any * field level validator fails. *

- * Validation is successful if the returned list is empty. * - * @return a list of validation errors or an empty list if validation - * succeeded + * @return validation status for the binder */ - public List> validate() { - List> errors = validateBindings(); - if (!errors.isEmpty()) { - return errors; - } - - if (bean != null) { - return validateItem(bean); + public BinderValidationStatus validate() { + List> bindingStatuses = validateBindings(); + + BinderValidationStatus validationStatus; + if (bindingStatuses.stream().filter(ValidationStatus::isError).findAny() + .isPresent() || bean == null) { + validationStatus = new BinderValidationStatus<>(this, + bindingStatuses, Collections.emptyList()); + } else { + validationStatus = new BinderValidationStatus<>(this, + bindingStatuses, validateItem(bean)); } - - return Collections.emptyList(); + getStatusHandler().accept(validationStatus); + return validationStatus; } /** * Validates the bindings and returns the result of the validation as a list - * of validation errors. - *

- * If all validators pass, the resulting list is empty. + * of validation statuses. *

* Does not run bean validators. - *

- * All results are passed to the {@link #getStatusHandler() status change - * handler.} * * @see #validateItem(Object) * - * @return a list of validation errors or an empty list if validation - * succeeded + * @return an immutable list of validation results for bindings */ - private List> validateBindings() { - List> results = new ArrayList<>(); + private List> validateBindings() { + List> results = new ArrayList<>(); for (BindingImpl binding : bindings) { results.add(binding.getTargetValue()); } - - getStatusHandler().accept(Collections.unmodifiableList(results)); - - return results.stream().filter(r -> r.isError()) - .map(r -> new ValidationError<>(r.getBinding().get(), - r.getField().get().getValue(), r.getMessage().get())) - .collect(Collectors.toList()); + return results; } /** * Validates the {@code item} using item validators added using * {@link #withValidator(Validator)} and returns the result of the - * validation as a list of validation errors. + * validation as a list of validation results. *

- * If all validators pass, the resulting list is empty. * * @see #withValidator(Validator) * @@ -964,20 +963,12 @@ public class Binder implements Serializable { * @return a list of validation errors or an empty list if validation * succeeded */ - private List> validateItem(BEAN bean) { + private List> validateItem(BEAN bean) { Objects.requireNonNull(bean, "bean cannot be null"); - List> results = Collections.unmodifiableList( + List> results = Collections.unmodifiableList( validators.stream().map(validator -> validator.apply(bean)) - .map(dataValue -> dataValue.biMap( - (value, message) -> new BinderResult<>(null, - value, message))) .collect(Collectors.toList())); - getStatusHandler().accept(results); - - return results.stream() - .filter(Result::isError).map(res -> new ValidationError<>(this, - bean, res.getMessage().get())) - .collect(Collectors.toList()); + return results; } /** @@ -1032,7 +1023,7 @@ public class Binder implements Serializable { * @throws NullPointerException * for null status handler * @see #setStatusLabel(Label) - * @see Binding#withStatusChangeHandler(StatusChangeHandler) + * @see Binding#withStatusHandler(StatusChangeHandler) */ public void setStatusHandler(BinderStatusHandler statusHandler) { Objects.requireNonNull(statusHandler, "Cannot set a null " @@ -1077,7 +1068,7 @@ public class Binder implements Serializable { */ protected BindingImpl createBinding( HasValue field, Converter converter, - StatusChangeHandler handler) { + ValidationStatusHandler handler) { return new BindingImpl<>(this, field, converter, handler); } @@ -1117,18 +1108,26 @@ public class Binder implements Serializable { /** * Default {@link StatusChangeHandler} functional method implementation. * - * @param event - * the validation event + * @param status + * the validation status */ - protected void handleValidationStatusChange( - ValidationStatusChangeEvent event) { - HasValue source = event.getSource(); + protected void handleValidationStatusChange(ValidationStatus status) { + HasValue source = status.getField(); clearError(source); - if (Objects.equals(ValidationStatus.ERROR, event.getStatus())) { - handleError(source, event.getMessage().get()); + if (status.isError()) { + handleError(source, status.getMessage().get()); } } + /** + * Returns the bindings for this binder. + * + * @return a set of the bindings + */ + protected Set> getBindings() { + return bindings; + } + /** * The default binder level status handler. *

@@ -1136,23 +1135,21 @@ public class Binder implements Serializable { * other status changes are displayed in the status label, if one has been * set with {@link #setStatusLabel(Label)}. * - * @param results - * a list of validation results from binding and/or item level + * @param binderStatus + * status of validation results from binding and/or item level * validators */ - @SuppressWarnings("unchecked") protected void defaultHandleBinderStatusChange( - List> results) { + BinderValidationStatus binderStatus) { // let field events go to binding status handlers - results.stream().filter(br -> br.getField().isPresent()) - .forEach(br -> ((BindingImpl) br.getBinding().get()) - .fireStatusChangeEvent(br)); + binderStatus.getFieldValidationStatuses() + .forEach(status -> ((BindingImpl) status.getBinding()) + .notifyStatusChangeHandler(status)); // show first possible error or OK status in the label if set if (getStatusLabel().isPresent()) { - String statusMessage = results.stream() - .filter(r -> !r.getField().isPresent()) - .map(Result::getMessage).map(m -> m.orElse("")).findFirst() + String statusMessage = binderStatus.getBeanValidationErrors() + .stream().findFirst().flatMap(Result::getMessage) .orElse(""); getStatusLabel().get().setValue(statusMessage); } diff --git a/server/src/main/java/com/vaadin/data/BinderResult.java b/server/src/main/java/com/vaadin/data/BinderResult.java deleted file mode 100644 index 52375b88ff..0000000000 --- a/server/src/main/java/com/vaadin/data/BinderResult.java +++ /dev/null @@ -1,72 +0,0 @@ -/* - * Copyright 2000-2016 Vaadin Ltd. - * - * Licensed under the Apache License, Version 2.0 (the "License"); you may not - * use this file except in compliance with the License. You may obtain a copy of - * the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT - * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the - * License for the specific language governing permissions and limitations under - * the License. - */ -package com.vaadin.data; - -import java.util.Optional; - -import com.vaadin.data.Binder.Binding; - -/** - * A result that keeps track of the possible binding (field) it belongs to. - * - * @param - * the value type of the field - * @param - * the result value type and the data type of the binding, matches - * the field type if a converter has not been set - */ -public class BinderResult extends SimpleResult { - - private final Binding binding; - - /** - * Creates a new binder result. - * - * @param binding - * the binding where the result originated, may be {@code null} - * @param value - * the resut value, can be null - * @param message - * the error message of the result, may be {@code null} - */ - public BinderResult(Binding binding, VALUE value, - String message) { - super(value, message); - this.binding = binding; - } - - /** - * Return the binding this result originated from, or an empty optional if - * none. - * - * @return the optional binding - */ - public Optional> getBinding() { - return Optional.ofNullable(binding); - } - - /** - * Return the field this result originated from, or an empty optional if - * none. - * - * @return the optional field - */ - public Optional> getField() { - return binding == null ? Optional.empty() - : Optional.ofNullable(binding.getField()); - } - -} \ No newline at end of file diff --git a/server/src/main/java/com/vaadin/data/BinderStatusHandler.java b/server/src/main/java/com/vaadin/data/BinderStatusHandler.java index 4c516a5ed6..5da5106176 100644 --- a/server/src/main/java/com/vaadin/data/BinderStatusHandler.java +++ b/server/src/main/java/com/vaadin/data/BinderStatusHandler.java @@ -16,7 +16,6 @@ package com.vaadin.data; import java.io.Serializable; -import java.util.List; import java.util.function.Consumer; import com.vaadin.data.Binder.Binding; @@ -34,13 +33,13 @@ import com.vaadin.data.Binder.Binding; * * @see Binder#setStatusHandler(BinderStatusHandler) * @see Binder#setStatusLabel(com.vaadin.ui.Label) - * @see Binding#withStatusChangeHandler(StatusChangeHandler) + * @see Binding#withStatusHandler(StatusChangeHandler) * * @author Vaadin Ltd * @since 8.0 * */ public interface BinderStatusHandler - extends Consumer>>, Serializable { + extends Consumer>, Serializable { } diff --git a/server/src/main/java/com/vaadin/data/BinderValidationStatus.java b/server/src/main/java/com/vaadin/data/BinderValidationStatus.java new file mode 100644 index 0000000000..f23d17e5b1 --- /dev/null +++ b/server/src/main/java/com/vaadin/data/BinderValidationStatus.java @@ -0,0 +1,161 @@ +/* + * Copyright 2000-2016 Vaadin Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ +package com.vaadin.data; + +import java.io.Serializable; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Objects; +import java.util.stream.Collectors; + +import com.vaadin.data.Binder.Binding; + +/** + * Binder validation status change. Represents the outcome of binder level + * validation. Has information about the validation results for the + * {@link Binding#withValidator(Validator) field level} and + * {@link Binder#withValidator(Validator)binder level} validation. + *

+ * Note: if there are any field level validation errors, the bean level + * validation is not run. + *

+ * Use {@link Binder#setStatusHandler(BinderStatusHandler)} to handle form level + * validation status changes. + * + * @author Vaadin Ltd + * + * @param + * the bean type of the binder + * + * @see BinderValidationStatusHandler + * @see Binder#setStatusHandler(BinderStatusHandler) + * @see Binder#validate() + * @see ValidationStatus + * + * @since 8.0 + */ +public class BinderValidationStatus implements Serializable { + + private final Binder binder; + private final List> bindingStatuses; + private final List> binderStatuses; + + /** + * Creates a new binder validation status for the given binder and + * validation results. + * + * @param source + * the source binder + * @param bindingStatuses + * the validation results for the fields + * @param binderStatuses + * the validation results for binder level validation + */ + public BinderValidationStatus(Binder source, + List> bindingStatuses, + List> binderStatuses) { + Objects.requireNonNull(binderStatuses, + "binding statuses cannot be null"); + Objects.requireNonNull(binderStatuses, + "binder statuses cannot be null"); + this.binder = source; + this.bindingStatuses = Collections.unmodifiableList(bindingStatuses); + this.binderStatuses = Collections.unmodifiableList(binderStatuses); + } + + /** + * Gets whether validation for the binder passed or not. + * + * @return {@code true} if validation has passed, {@code false} if not + */ + public boolean isOk() { + return !hasErrors(); + } + + /** + * Gets whether the validation for the binder failed or not. + * + * @return {@code true} if validation failed, {@code false} if validation + * passed + */ + public boolean hasErrors() { + return binderStatuses.stream().filter(Result::isError).findAny() + .isPresent() + || bindingStatuses.stream().filter(ValidationStatus::isError) + .findAny().isPresent(); + } + + /** + * Gets the source binder of the status. + * + * @return the source binder + */ + public Binder getBinder() { + return binder; + } + + /** + * Gets both field and bean level validation errors. + * + * @return a list of all validation errors + */ + public List> getValidationErrors() { + ArrayList> errors = new ArrayList<>(getFieldValidationErrors() + .stream().map(s -> s.getResult().get()) + .collect(Collectors.toList())); + errors.addAll(getBeanValidationErrors()); + return errors; + } + + /** + * Gets the field level validation statuses. + * + * @return the field validation statuses + */ + public List> getFieldValidationStatuses() { + return bindingStatuses; + } + + /** + * Gets the bean level validation results. + * + * @return the bean level validation results + */ + public List> getBeanValidationResults() { + return binderStatuses; + } + + /** + * Gets the failed field level validation statuses. + * + * @return a list of failed field level validation statuses + */ + public List> getFieldValidationErrors() { + return bindingStatuses.stream().filter(ValidationStatus::isError) + .collect(Collectors.toList()); + } + + /** + * Gets the failed bean level validation statuses. + * + * @return a list of failed bean level validation statuses + */ + public List> getBeanValidationErrors() { + return binderStatuses.stream().filter(Result::isError) + .collect(Collectors.toList()); + } +} diff --git a/server/src/main/java/com/vaadin/data/BinderValidationStatusHandler.java b/server/src/main/java/com/vaadin/data/BinderValidationStatusHandler.java new file mode 100644 index 0000000000..9528c5a884 --- /dev/null +++ b/server/src/main/java/com/vaadin/data/BinderValidationStatusHandler.java @@ -0,0 +1,50 @@ +/* + * Copyright 2000-2016 Vaadin Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ +package com.vaadin.data; + +import java.io.Serializable; +import java.util.function.Consumer; + +import com.vaadin.ui.AbstractComponent; + +/** + * Handler for {@link BinderValidationStatus} changes. + *

+ * {{@link Binder#setStatusHandler(BinderStatusHandler) Register} an instance of + * this class to be able to customize validation status handling. + *

+ * The default handler will show + * {@link AbstractComponent#setComponentError(com.vaadin.server.ErrorMessage) an + * error message} for failed field validations. For bean level validation errors + * it will display the first error message in + * {@link Binder#setStatusLabel(com.vaadin.ui.Label) status label}, if one has + * been set. + * + * @author Vaadin Ltd + * + * @param + * the bean type of the binder + * + * @see BinderValidationStatus + * @see Binder#validate() + * @see ValidationStatus + * + * @since 8.0 + */ +public interface BinderValidationStatusHandler + extends Consumer>, Serializable { + +} diff --git a/server/src/main/java/com/vaadin/data/ValidationError.java b/server/src/main/java/com/vaadin/data/ValidationError.java deleted file mode 100644 index 1555ecad2c..0000000000 --- a/server/src/main/java/com/vaadin/data/ValidationError.java +++ /dev/null @@ -1,104 +0,0 @@ -/* - * Copyright 2000-2016 Vaadin Ltd. - * - * Licensed under the Apache License, Version 2.0 (the "License"); you may not - * use this file except in compliance with the License. You may obtain a copy of - * the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT - * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the - * License for the specific language governing permissions and limitations under - * the License. - */ -package com.vaadin.data; - -import java.io.Serializable; -import java.util.Objects; -import java.util.Optional; - -import com.vaadin.data.Binder.Binding; - -/** - * Represents a validation error. - *

- * A validation error is either connected to a field validator ( - * {@link #getField()} returns a non-empty optional) or to an item level - * validator ({@link #getField()} returns an empty optional). - * - * @author Vaadin Ltd - * @since 8.0 - * - * @param - * the value type - */ -public class ValidationError implements Serializable { - - /** - * This is either a {@link Binding} or a {@link Binder}. - */ - private final Object source; - private final String message; - /** - * This is either HasValue value (in case of Binding) or bean (in case of - * Binder). - */ - private final V value; - - /** - * Creates a new instance of ValidationError using the provided source - * ({@link Binding} or {@link Binder}), value and error message. - * - * @param source - * the validated binding or the binder - * @param value - * the invalid value - * @param message - * the validation error message, not {@code null} - */ - public ValidationError(Object source, V value, String message) { - Objects.requireNonNull(message, "message cannot be null"); - this.source = source; - this.message = message; - this.value = value; - } - - /** - * Returns a reference to the validated field or an empty optional if the - * validation was not related to a single field. - * - * @return the validated field or an empty optional - */ - @SuppressWarnings({ "unchecked", "rawtypes" }) - public Optional> getField() { - if (source instanceof Binding) { - return Optional.of(((Binding) source).getField()); - } else { - return Optional.empty(); - } - } - - /** - * Returns a validation error message. - * - * @return the validation error message - */ - public String getMessage() { - return message; - } - - /** - * Returns the invalid value. - *

- * This is either the field value (if the validator error comes from a field - * binding) or the bean (for item validators). - * - * @return the source value - */ - public V getValue() { - return value; - } - -} diff --git a/server/src/main/java/com/vaadin/data/ValidationException.java b/server/src/main/java/com/vaadin/data/ValidationException.java index 41017a8393..68f7ad9e33 100644 --- a/server/src/main/java/com/vaadin/data/ValidationException.java +++ b/server/src/main/java/com/vaadin/data/ValidationException.java @@ -15,39 +15,75 @@ */ package com.vaadin.data; +import java.util.ArrayList; import java.util.Collections; import java.util.List; +import java.util.stream.Collectors; /** * Indicates validation errors in a {@link Binder} when save is requested. - * + * * @see Binder#save(Object) - * + * * @author Vaadin Ltd * @since 8.0 * */ public class ValidationException extends Exception { - private final List> errors; + private final List> bindingValidationErrors; + private final List> binderValidationErrors; /** * Constructs a new exception with validation {@code errors} list. - * - * @param errors - * validation errors list + * + * @param bindingValidationErrors + * binding validation errors list + * @param binderValidationErrors + * binder validation errors list */ - public ValidationException(List> errors) { + public ValidationException( + List> bindingValidationErrors, + List> binderValidationErrors) { super("Validation has failed for some fields"); - this.errors = Collections.unmodifiableList(errors); + this.bindingValidationErrors = Collections + .unmodifiableList(bindingValidationErrors); + this.binderValidationErrors = Collections + .unmodifiableList(binderValidationErrors); } /** - * Returns the validation errors list which caused the exception. - * - * @return validation errors list + * Gets both field and bean level validation errors. + * + * @return a list of all validation errors */ - public List> getValidationError() { + public List> getValidationErrors() { + ArrayList> errors = new ArrayList<>(getFieldValidationErrors() + .stream().map(s -> s.getResult().get()) + .collect(Collectors.toList())); + errors.addAll(getBeanValidationErrors()); return errors; } + + /** + * Returns a list of the binding level validation errors which caused the + * exception, or an empty list if was caused by + * {@link #getBeanValidationErrors() binder level validation errors}. + * + * @return binding validation errors list + */ + public List> getFieldValidationErrors() { + return bindingValidationErrors; + } + + /** + * Returns a list of the binding level validation errors which caused the + * exception, or an empty list if was caused by + * {@link #getBeanValidationErrors() binder level validation errors}. + * + * @return binder validation errors list + */ + public List> getBeanValidationErrors() { + return binderValidationErrors; + } } diff --git a/server/src/main/java/com/vaadin/data/ValidationStatus.java b/server/src/main/java/com/vaadin/data/ValidationStatus.java index 03f76c5127..7203655e7b 100644 --- a/server/src/main/java/com/vaadin/data/ValidationStatus.java +++ b/server/src/main/java/com/vaadin/data/ValidationStatus.java @@ -15,21 +15,147 @@ */ package com.vaadin.data; +import java.io.Serializable; +import java.util.Objects; +import java.util.Optional; + import com.vaadin.data.Binder.Binding; /** - * Validation status. - *

- * The status is the part of {@link ValidationStatusChangeEvent} which indicate - * whether the validation failed or not. - * - * @see ValidationStatusChangeEvent - * @see Binding#withStatusChangeHandler(StatusChangeHandler) - * @see StatusChangeHandler + * Represents the outcome of field level validation. Use + * {@link Binding#withStatusChangeHandler(ValidationStatusHandler)} to register + * a handler for field level validation status changes. * * @author Vaadin Ltd + * + * @param + * the target data type of the binding for which the validation + * status changed, matches the field type until a converter has been + * set + * + * @see Binding#withStatusChangeHandler(ValidationStatusHandler) + * @see Binding#validate() + * @see ValidationStatusHandler + * @see BinderValidationStatus + * * @since 8.0 */ -public enum ValidationStatus { - OK, ERROR; +public class ValidationStatus implements Serializable { + + /** + * Status of the validation. + *

+ * The status is the part of {@link ValidationStatus} which indicates + * whether the validation failed or not. + */ + public enum Status { + /** Validation passed. */ + OK, + /** Validation failed. */ + ERROR + } + + private final Status status; + private final Result result; + private final Binding binding; + + /** + * Creates a new validation status for the given binding and validation + * result. + * + * @param source + * the source binding + * @param result + * the result of the validation + */ + public ValidationStatus(Binding source, + Result result) { + this(source, result.isError() ? Status.ERROR : Status.OK, result); + } + + /** + * Creates a new status change event. + *

+ * The {@code message} must be {@code null} if the {@code status} is + * {@link Status#OK}. + * + * @param source + * field whose status has changed, not {@code null} + * @param status + * updated status value, not {@code null} + * @param result + * the related result, may be {@code null} + */ + public ValidationStatus(Binding source, Status status, + Result result) { + Objects.requireNonNull(source, "Event source may not be null"); + Objects.requireNonNull(status, "Status may not be null"); + if (Objects.equals(status, Status.OK) && result.isError() + || Objects.equals(status, Status.ERROR) && !result.isError()) { + throw new IllegalStateException( + "Invalid validation status " + status + " for given result " + + (result == null ? "null" : result.toString())); + } + binding = source; + this.status = status; + this.result = result; + } + + /** + * Gets status of the validation. + * + * @return status + */ + public Status getStatus() { + return status; + } + + /** + * Gets whether the validation failed or not. + * + * @return {@code true} if validation failed, {@code false} if validation + * passed + */ + public boolean isError() { + return status == Status.ERROR; + } + + /** + * Gets error validation message if status is {@link Status#ERROR}. + * + * @return an optional validation error status or an empty optional if + * status is not an error + */ + public Optional getMessage() { + return Optional.ofNullable(result).flatMap(Result::getMessage); + } + + /** + * Gets the validation result if status is either {@link Status#OK} or + * {@link Status#ERROR} or an empty optional if status is + * {@link Status#UNRESOLVED}. + * + * @return the validation result + */ + public Optional> getResult() { + return Optional.ofNullable(result); + } + + /** + * Gets the source binding of the validation status. + * + * @return the source binding + */ + public Binding getBinding() { + return binding; + } + + /** + * Gets the bound field for this status. + * + * @return the field + */ + public HasValue getField() { + return getBinding().getField(); + } } diff --git a/server/src/main/java/com/vaadin/data/ValidationStatusChangeEvent.java b/server/src/main/java/com/vaadin/data/ValidationStatusChangeEvent.java deleted file mode 100644 index e021398341..0000000000 --- a/server/src/main/java/com/vaadin/data/ValidationStatusChangeEvent.java +++ /dev/null @@ -1,93 +0,0 @@ -/* - * Copyright 2000-2016 Vaadin Ltd. - * - * Licensed under the Apache License, Version 2.0 (the "License"); you may not - * use this file except in compliance with the License. You may obtain a copy of - * the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT - * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the - * License for the specific language governing permissions and limitations under - * the License. - */ -package com.vaadin.data; - -import java.util.EventObject; -import java.util.Objects; -import java.util.Optional; - -import com.vaadin.data.Binder.Binding; - -/** - * Validation status change event which is fired each time when validation is - * done. Use {@link Binding#withStatusChangeHandler(StatusChangeHandler)} method - * to add your validation handler to listen to the event. - * - * @see Binding#withStatusChangeHandler(StatusChangeHandler) - * @see StatusChangeHandler - * - * @author Vaadin Ltd - * @since 8.0 - * - */ -public class ValidationStatusChangeEvent extends EventObject { - - private final ValidationStatus status; - private final String message; - - /** - * Creates a new status change event. - *

- * The {@code message} must be null if the {@code status} is - * {@link ValidationStatus#OK}. - * - * @param source - * field whose status has changed, not {@code null} - * @param status - * updated status value, not {@code null} - * @param message - * error message if status is ValidationStatus.ERROR, may be - * {@code null} - */ - public ValidationStatusChangeEvent(HasValue source, - ValidationStatus status, String message) { - super(source); - Objects.requireNonNull(source, "Event source may not be null"); - Objects.requireNonNull(status, "Status may not be null"); - if (Objects.equals(status, ValidationStatus.OK) && message != null) { - throw new IllegalStateException( - "Message must be null if status is not an error"); - } - this.status = status; - this.message = message; - } - - /** - * Returns validation status of the event. - * - * @return validation status - */ - public ValidationStatus getStatus() { - return status; - } - - /** - * Returns error validation message if status is - * {@link ValidationStatus#ERROR}. - * - * @return an optional validation error status or an empty optional if - * status is not an error - */ - public Optional getMessage() { - return Optional.ofNullable(message); - } - - @Override - public HasValue getSource() { - return (HasValue) super.getSource(); - } - -} diff --git a/server/src/main/java/com/vaadin/data/StatusChangeHandler.java b/server/src/main/java/com/vaadin/data/ValidationStatusHandler.java similarity index 58% rename from server/src/main/java/com/vaadin/data/StatusChangeHandler.java rename to server/src/main/java/com/vaadin/data/ValidationStatusHandler.java index 790ac74fb1..932fe64575 100644 --- a/server/src/main/java/com/vaadin/data/StatusChangeHandler.java +++ b/server/src/main/java/com/vaadin/data/ValidationStatusHandler.java @@ -19,22 +19,25 @@ import java.io.Serializable; import java.util.function.Consumer; import com.vaadin.data.Binder.Binding; +import com.vaadin.ui.AbstractComponent; /** - * Validation status change handler. + * Handler for {@link ValidationStatus} changes. *

- * Register an instance of this class using - * {@link Binding#withStatusChangeHandler(StatusChangeHandler) to be able to - * listen to validation status updates. - * - * @see Binding#withStatusChangeHandler(StatusChangeHandler) - * @see ValidationStatusChangeEvent + * {@link Binding#withStatusHandler(StatusChangeHandler) Register} an instance + * of this class to be able to override the default handling, which is to show + * {@link AbstractComponent#setComponentError(com.vaadin.server.ErrorMessage) an + * error message} for failed field validations. * * @author Vaadin Ltd + * + * @see Binding#withStatusHandler(StatusChangeHandler) + * @see ValidationStatus + * * @since 8.0 * */ -public interface StatusChangeHandler - extends Consumer, Serializable { +public interface ValidationStatusHandler + extends Consumer>, Serializable { } diff --git a/server/src/test/java/com/vaadin/data/BeanBinderTest.java b/server/src/test/java/com/vaadin/data/BeanBinderTest.java index 1788c3a9e8..84a414edf1 100644 --- a/server/src/test/java/com/vaadin/data/BeanBinderTest.java +++ b/server/src/test/java/com/vaadin/data/BeanBinderTest.java @@ -163,9 +163,10 @@ public class BeanBinderTest { } private void assertInvalid(HasValue field, String message) { - List> errors = binder.validate(); + BinderValidationStatus status = binder.validate(); + List> errors = status.getFieldValidationErrors(); assertEquals(1, errors.size()); - assertSame(field, errors.get(0).getField().get()); - assertEquals(message, errors.get(0).getMessage()); + assertSame(field, errors.get(0).getField()); + assertEquals(message, errors.get(0).getMessage().get()); } } diff --git a/server/src/test/java/com/vaadin/data/BinderBookOfVaadinTest.java b/server/src/test/java/com/vaadin/data/BinderBookOfVaadinTest.java index 6861903132..d11a0ed6da 100644 --- a/server/src/test/java/com/vaadin/data/BinderBookOfVaadinTest.java +++ b/server/src/test/java/com/vaadin/data/BinderBookOfVaadinTest.java @@ -27,12 +27,14 @@ import org.junit.Before; import org.junit.Test; import com.vaadin.data.Binder.Binding; +import com.vaadin.data.ValidationStatus.Status; import com.vaadin.data.util.converter.Converter; import com.vaadin.data.util.converter.StringToIntegerConverter; import com.vaadin.data.validator.EmailValidator; import com.vaadin.server.AbstractErrorMessage; import com.vaadin.ui.Button; import com.vaadin.ui.Label; +import com.vaadin.ui.Notification; import com.vaadin.ui.PopupDateField; import com.vaadin.ui.Slider; import com.vaadin.ui.TextField; @@ -139,6 +141,30 @@ public class BinderBookOfVaadinTest { emailField = new TextField(); } + @Test + public void loadingFromBusinessObjects() { + // this test is just to make sure the code snippet in the book compiles + binder.load(new BookPerson(1969, 50000)); + + BinderValidationStatus status = binder.validate(); + + if (status.hasErrors()) { + Notification.show("Validation error count: " + + status.getValidationErrors().size()); + } + } + + @Test + public void handlingCheckedException() { + // another test just to verify that book examples actually compile + try { + binder.save(new BookPerson(2000, 50000)); + } catch (ValidationException e) { + Notification.show("Validation error count: " + + e.getValidationErrors().size()); + } + } + @Test public void simpleEmailValidator() { binder.forField(field) @@ -148,16 +174,16 @@ public class BinderBookOfVaadinTest { .bind(BookPerson::getEmail, BookPerson::setEmail); field.setValue("not-email"); - List> errors = binder.validate(); - Assert.assertEquals(1, errors.size()); + BinderValidationStatus status = binder.validate(); + Assert.assertEquals(1, status.getFieldValidationErrors().size()); Assert.assertEquals("This doesn't look like a valid email address", - errors.get(0).getMessage()); + status.getFieldValidationErrors().get(0).getMessage().get()); Assert.assertEquals("This doesn't look like a valid email address", ((AbstractErrorMessage) field.getErrorMessage()).getMessage()); field.setValue("abc@vaadin.com"); - errors = binder.validate(); - Assert.assertEquals(0, errors.size()); + status = binder.validate(); + Assert.assertEquals(0, status.getBeanValidationErrors().size()); Assert.assertNull(field.getErrorMessage()); } @@ -170,16 +196,16 @@ public class BinderBookOfVaadinTest { .bind(BookPerson::getLastName, BookPerson::setLastName); field.setValue("a"); - List> errors = binder.validate(); - Assert.assertEquals(1, errors.size()); + BinderValidationStatus status = binder.validate(); + Assert.assertEquals(1, status.getFieldValidationErrors().size()); Assert.assertEquals("Last name must contain at least three characters", - errors.get(0).getMessage()); + status.getFieldValidationErrors().get(0).getMessage().get()); Assert.assertEquals("Last name must contain at least three characters", ((AbstractErrorMessage) field.getErrorMessage()).getMessage()); field.setValue("long last name"); - errors = binder.validate(); - Assert.assertEquals(0, errors.size()); + status = binder.validate(); + Assert.assertEquals(0, status.getFieldValidationErrors().size()); Assert.assertNull(field.getErrorMessage()); } @@ -194,25 +220,25 @@ public class BinderBookOfVaadinTest { .bind(BookPerson::getEmail, BookPerson::setEmail); field.setValue("not-email"); - List> errors = binder.validate(); + BinderValidationStatus status = binder.validate(); // Only one error per field should be reported - Assert.assertEquals(1, errors.size()); + Assert.assertEquals(1, status.getFieldValidationErrors().size()); Assert.assertEquals("This doesn't look like a valid email address", - errors.get(0).getMessage()); + status.getFieldValidationErrors().get(0).getMessage().get()); Assert.assertEquals("This doesn't look like a valid email address", ((AbstractErrorMessage) field.getErrorMessage()).getMessage()); field.setValue("abc@vaadin.com"); - errors = binder.validate(); - Assert.assertEquals(1, errors.size()); + status = binder.validate(); + Assert.assertEquals(1, status.getFieldValidationErrors().size()); Assert.assertEquals("Only acme.com email addresses are allowed", - errors.get(0).getMessage()); + status.getFieldValidationErrors().get(0).getMessage().get()); Assert.assertEquals("Only acme.com email addresses are allowed", ((AbstractErrorMessage) field.getErrorMessage()).getMessage()); field.setValue("abc@acme.com"); - errors = binder.validate(); - Assert.assertEquals(0, errors.size()); + status = binder.validate(); + Assert.assertEquals(0, status.getFieldValidationErrors().size()); Assert.assertNull(field.getErrorMessage()); } @@ -296,32 +322,32 @@ public class BinderBookOfVaadinTest { departing.setValue(before); returning.setValue(after); - List> errors = binder.validate(); - Assert.assertTrue(errors.isEmpty()); + BinderValidationStatus status = binder.validate(); + Assert.assertTrue(status.getBeanValidationErrors().isEmpty()); Assert.assertNull(departing.getComponentError()); Assert.assertNull(returning.getComponentError()); // update returning => validation is done against this field returning.setValue(past); - errors = binder.validate(); + status = binder.validate(); - Assert.assertFalse(errors.isEmpty()); + Assert.assertFalse(status.getFieldValidationErrors().isEmpty()); Assert.assertNotNull(returning.getComponentError()); Assert.assertNull(departing.getComponentError()); // set correct value back returning.setValue(before); - errors = binder.validate(); + status = binder.validate(); - Assert.assertTrue(errors.isEmpty()); + Assert.assertTrue(status.getFieldValidationErrors().isEmpty()); Assert.assertNull(departing.getComponentError()); Assert.assertNull(returning.getComponentError()); // update departing => validation is done because of listener added departing.setValue(after); - errors = binder.validate(); + status = binder.validate(); - Assert.assertFalse(errors.isEmpty()); + Assert.assertFalse(status.getFieldValidationErrors().isEmpty()); Assert.assertNotNull(returning.getComponentError()); Assert.assertNull(departing.getComponentError()); @@ -351,7 +377,7 @@ public class BinderBookOfVaadinTest { departing.setValue(before); returning.setValue(after); - Result result = returnBinding.validate(); + ValidationStatus result = returnBinding.validate(); Assert.assertFalse(result.isError()); Assert.assertNull(departing.getComponentError()); @@ -402,17 +428,16 @@ public class BinderBookOfVaadinTest { @Test public void withBindingStatusChangeHandlerExample() { Label nameStatus = new Label(); - AtomicReference event = new AtomicReference<>(); + AtomicReference> statusCapture = new AtomicReference<>(); String msg = "Full name must contain at least three characters"; binder.forField(field).withValidator(name -> name.length() >= 3, msg) - .withStatusChangeHandler(statusChange -> { + .withStatusHandler(statusChange -> { nameStatus.setValue(statusChange.getMessage().orElse("")); // Only show the label when validation has failed - boolean error = statusChange - .getStatus() == ValidationStatus.ERROR; + boolean error = statusChange.getStatus() == Status.ERROR; nameStatus.setVisible(error); - event.set(statusChange); + statusCapture.set(statusChange); }).bind(BookPerson::getLastName, BookPerson::setLastName); field.setValue("aa"); @@ -420,22 +445,22 @@ public class BinderBookOfVaadinTest { Assert.assertTrue(nameStatus.isVisible()); Assert.assertEquals(msg, nameStatus.getValue()); - Assert.assertNotNull(event.get()); - ValidationStatusChangeEvent evt = event.get(); - Assert.assertEquals(ValidationStatus.ERROR, evt.getStatus()); - Assert.assertEquals(msg, evt.getMessage().get()); - Assert.assertEquals(field, evt.getSource()); + Assert.assertNotNull(statusCapture.get()); + ValidationStatus status = statusCapture.get(); + Assert.assertEquals(Status.ERROR, status.getStatus()); + Assert.assertEquals(msg, status.getMessage().get()); + Assert.assertEquals(field, status.getField()); field.setValue("foo"); binder.validate(); Assert.assertFalse(nameStatus.isVisible()); Assert.assertEquals("", nameStatus.getValue()); - Assert.assertNotNull(event.get()); - evt = event.get(); - Assert.assertEquals(ValidationStatus.OK, evt.getStatus()); - Assert.assertFalse(evt.getMessage().isPresent()); - Assert.assertEquals(field, evt.getSource()); + Assert.assertNotNull(statusCapture.get()); + status = statusCapture.get(); + Assert.assertEquals(Status.OK, status.getStatus()); + Assert.assertFalse(status.getMessage().isPresent()); + Assert.assertEquals(field, status.getField()); } @Test @@ -495,17 +520,18 @@ public class BinderBookOfVaadinTest { .bind(BookPerson::getYearOfBirth, BookPerson::setYearOfBirth); yearOfBirthField.setValue("abc"); - Assert.assertEquals("Doesn't look like a year", - binder.validate().get(0).getMessage()); + Assert.assertEquals("Doesn't look like a year", binder.validate() + .getFieldValidationErrors().get(0).getMessage().get()); yearOfBirthField.setValue("abcd"); - Assert.assertEquals("Must enter a number", - binder.validate().get(0).getMessage()); + Assert.assertEquals("Must enter a number", binder.validate() + .getFieldValidationErrors().get(0).getMessage().get()); yearOfBirthField.setValue("1200"); Assert.assertEquals("Person must be born in the 20th century", - binder.validate().get(0).getMessage()); + binder.validate().getFieldValidationErrors().get(0).getMessage() + .get()); yearOfBirthField.setValue("1950"); - Assert.assertTrue(binder.validate().isEmpty()); + Assert.assertFalse(binder.validate().hasErrors()); BookPerson person = new BookPerson(1500, 12); binder.save(person); Assert.assertEquals(1950, person.getYearOfBirth()); @@ -546,15 +572,17 @@ public class BinderBookOfVaadinTest { binder.bind(p); yearOfBirthField.setValue("abc"); - Assert.assertEquals("Please enter a number", - binder.validate().get(0).getMessage()); + Assert.assertTrue(binder.validate().hasErrors()); + Assert.assertEquals("Please enter a number", binder.validate() + .getFieldValidationErrors().get(0).getMessage().get()); yearOfBirthField.setValue("123"); - Assert.assertTrue(binder.validate().isEmpty()); + Assert.assertTrue(binder.validate().isOk()); p.setYearOfBirth(12500); binder.load(p); Assert.assertEquals("12500", yearOfBirthField.getValue()); + Assert.assertTrue(binder.validate().isOk()); } @Test @@ -582,25 +610,30 @@ public class BinderBookOfVaadinTest { // first bean validator fails and passes error message to status label yearOfBirth.setValue("2001"); - List> errors = binder.validate(); - Assert.assertEquals(1, errors.size()); - Assert.assertEquals(errors.get(0).getMessage(), message); + BinderValidationStatus status = binder.validate(); + Assert.assertEquals(0, status.getFieldValidationErrors().size()); + Assert.assertEquals(1, status.getBeanValidationErrors().size()); + Assert.assertEquals( + status.getBeanValidationErrors().get(0).getMessage().get(), + message); Assert.assertEquals(message, formStatusLabel.getValue()); // value is correct, status label is cleared yearOfBirth.setValue("1999"); - errors = binder.validate(); - Assert.assertEquals(0, errors.size()); + status = binder.validate(); + Assert.assertFalse(status.hasErrors()); Assert.assertEquals("", formStatusLabel.getValue()); // both bean validators fail, should be two error messages chained yearOfBirth.setValue("2000"); - errors = binder.validate(); - Assert.assertEquals(2, errors.size()); + status = binder.validate(); + Assert.assertEquals(2, status.getBeanValidationResults().size()); + Assert.assertEquals(0, status.getFieldValidationErrors().size()); + Assert.assertEquals(2, status.getBeanValidationErrors().size()); // only first error is shown Assert.assertEquals(message, formStatusLabel.getValue()); @@ -612,21 +645,17 @@ public class BinderBookOfVaadinTest { BinderStatusHandler defaultHandler = binder.getStatusHandler(); - binder.setStatusHandler(results -> { - String errorMessage = results.stream() - // Ignore confirmation messages - .filter(BinderResult::isError) - // Ignore messages that belong to a specific field - .filter(error -> !error.getField().isPresent()) - // Create a string out of the remaining messages - .map(Result::getMessage).map(o -> o.get()) - .collect(Collectors.joining("\n")); - + binder.setStatusHandler(status -> { + // create an error message on failed bean level validations + List> errors = status.getBeanValidationErrors(); + String errorMessage = errors.stream().map(Result::getMessage) + .map(o -> o.get()).collect(Collectors.joining("\n")); + // show error in a label formStatusLabel.setValue(errorMessage); formStatusLabel.setVisible(!errorMessage.isEmpty()); // Let the default handler show messages for each field - defaultHandler.accept(results); + defaultHandler.accept(status); }); final String bindingMessage = "uneven"; @@ -648,34 +677,41 @@ public class BinderBookOfVaadinTest { // first binding validation fails, no bean level validation is done yearOfBirth.setValue("2001"); - List> errors = binder.validate(); - Assert.assertEquals(1, errors.size()); - Assert.assertEquals(errors.get(0).getMessage(), bindingMessage); + BinderValidationStatus status = binder.validate(); + Assert.assertEquals(1, status.getFieldValidationErrors().size()); + Assert.assertEquals(bindingMessage, + status.getFieldValidationErrors().get(0).getMessage().get()); Assert.assertEquals("", formStatusLabel.getValue()); // first bean validator fails and passes error message to status label yearOfBirth.setValue("2002"); - errors = binder.validate(); - Assert.assertEquals(1, errors.size()); - Assert.assertEquals(errors.get(0).getMessage(), message); + status = binder.validate(); + Assert.assertEquals(0, status.getFieldValidationErrors().size()); + Assert.assertEquals(1, status.getBeanValidationErrors().size()); + Assert.assertEquals(message, + status.getBeanValidationErrors().get(0).getMessage().get()); Assert.assertEquals(message, formStatusLabel.getValue()); // value is correct, status label is cleared yearOfBirth.setValue("1998"); - errors = binder.validate(); - Assert.assertEquals(0, errors.size()); + status = binder.validate(); + Assert.assertTrue(status.isOk()); + Assert.assertFalse(status.hasErrors()); + Assert.assertEquals(0, status.getFieldValidationErrors().size()); + Assert.assertEquals(0, status.getBeanValidationErrors().size()); Assert.assertEquals("", formStatusLabel.getValue()); // both bean validators fail, should be two error messages chained yearOfBirth.setValue("2000"); - errors = binder.validate(); - Assert.assertEquals(2, errors.size()); + status = binder.validate(); + Assert.assertEquals(0, status.getFieldValidationErrors().size()); + Assert.assertEquals(2, status.getBeanValidationErrors().size()); Assert.assertEquals(message + "\n" + message2, formStatusLabel.getValue()); diff --git a/server/src/test/java/com/vaadin/data/BinderTest.java b/server/src/test/java/com/vaadin/data/BinderTest.java index 4b69fdf50e..0a82b527bb 100644 --- a/server/src/test/java/com/vaadin/data/BinderTest.java +++ b/server/src/test/java/com/vaadin/data/BinderTest.java @@ -5,17 +5,15 @@ import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertSame; import java.util.List; -import java.util.Optional; -import java.util.Set; import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicReference; -import java.util.stream.Collectors; import org.junit.Assert; import org.junit.Before; import org.junit.Test; import com.vaadin.data.Binder.Binding; +import com.vaadin.data.ValidationStatus.Status; import com.vaadin.data.util.converter.Converter; import com.vaadin.data.validator.NotEmptyValidator; import com.vaadin.server.AbstractErrorMessage; @@ -27,6 +25,8 @@ import com.vaadin.ui.TextField; public class BinderTest { + private static final String EMPTY_ERROR_MESSAGE = "Value cannot be empty"; + private static class StatusBean { private String status; @@ -48,7 +48,7 @@ public class BinderTest { Person p = new Person(); Validator notEmpty = Validator.from(val -> !val.isEmpty(), - "Value cannot be empty"); + EMPTY_ERROR_MESSAGE); Converter stringToInteger = Converter.from( Integer::valueOf, String::valueOf, e -> "Value must be a number"); Validator notNegative = Validator.from(x -> x >= 0, @@ -327,18 +327,17 @@ public class BinderTest { binder.save(person); Assert.fail(); } catch (ValidationException exception) { - List> validationErrors = exception - .getValidationError(); + List> validationErrors = exception + .getFieldValidationErrors(); Assert.assertEquals(2, validationErrors.size()); - ValidationError error = validationErrors.get(0); - Assert.assertEquals(nameField, error.getField().get()); - Assert.assertEquals(msg, error.getMessage()); - Assert.assertEquals("", error.getValue()); + ValidationStatus error = validationErrors.get(0); + Assert.assertEquals(nameField, error.getField()); + Assert.assertEquals(msg, error.getMessage().get()); error = validationErrors.get(1); - Assert.assertEquals(ageField, error.getField().get()); - Assert.assertEquals("Value must be positive", error.getMessage()); - Assert.assertEquals(ageField.getValue(), error.getValue()); + Assert.assertEquals(ageField, error.getField()); + Assert.assertEquals("Value must be positive", + error.getMessage().get()); } } @@ -375,9 +374,9 @@ public class BinderTest { public void validate_notBound_noErrors() { Binder binder = new Binder<>(); - List> errors = binder.validate(); + BinderValidationStatus status = binder.validate(); - Assert.assertTrue(errors.isEmpty()); + Assert.assertTrue(status.isOk()); } @Test @@ -388,9 +387,9 @@ public class BinderTest { Person::setFirstName); nameField.setComponentError(new UserError("")); - List> errors = binder.validate(); + BinderValidationStatus status = binder.validate(); - Assert.assertTrue(errors.isEmpty()); + Assert.assertTrue(status.isOk()); Assert.assertNull(nameField.getComponentError()); } @@ -411,19 +410,19 @@ public class BinderTest { binding.withValidator(value -> false, msg2); binding.bind(Person::getFirstName, Person::setFirstName); - List> errors = binder.validate(); + BinderValidationStatus status = binder.validate(); + List> errors = status.getFieldValidationErrors(); Assert.assertEquals(1, errors.size()); - Set errorMessages = errors.stream() - .map(ValidationError::getMessage).collect(Collectors.toSet()); - Assert.assertTrue(errorMessages.contains(msg1)); + ValidationStatus validationStatus = errors.stream().findFirst() + .get(); + String msg = validationStatus.getMessage().get(); + Assert.assertEquals(msg1, msg); + + HasValue field = validationStatus.getField(); - Set fields = errors.stream().map(ValidationError::getField) - .filter(Optional::isPresent).map(Optional::get) - .collect(Collectors.toSet()); - Assert.assertEquals(1, fields.size()); - Assert.assertTrue(fields.contains(nameField)); + Assert.assertEquals(nameField, field); ErrorMessage componentError = nameField.getComponentError(); Assert.assertNotNull(componentError); @@ -490,19 +489,25 @@ public class BinderTest { ageField.setValue(""); assertEquals(32, p.getAge()); - assertValidationErrors(binder.validate(), "Value cannot be empty"); + assertValidationErrors(binder.validate(), EMPTY_ERROR_MESSAGE); } private void assertValidationErrors( - List> validationErrors, + List> validationErrors, String... errorMessages) { Assert.assertEquals(errorMessages.length, validationErrors.size()); for (int i = 0; i < errorMessages.length; i++) { Assert.assertEquals(errorMessages[i], - validationErrors.get(i).getMessage()); + validationErrors.get(i).getMessage().get()); } } + private void assertValidationErrors(BinderValidationStatus status, + String... errorMessages) { + assertValidationErrors(status.getFieldValidationErrors(), + errorMessages); + } + @Test public void convertToModelConversionFails() { bindAgeWithValidatorConverterValidator(); @@ -571,11 +576,11 @@ public class BinderTest { @Test public void bindingWithStatusChangeHandler_handlerGetsEvents() { - AtomicReference event = new AtomicReference<>(); + AtomicReference> statusCapture = new AtomicReference<>(); Binding binding = binder.forField(nameField) - .withValidator(notEmpty).withStatusChangeHandler(evt -> { - Assert.assertNull(event.get()); - event.set(evt); + .withValidator(notEmpty).withStatusHandler(evt -> { + Assert.assertNull(statusCapture.get()); + statusCapture.set(evt); }); binding.bind(Person::getFirstName, Person::setFirstName); @@ -585,30 +590,30 @@ public class BinderTest { // message binder.validate(); - Assert.assertNotNull(event.get()); - ValidationStatusChangeEvent evt = event.get(); - Assert.assertEquals(ValidationStatus.ERROR, evt.getStatus()); - Assert.assertEquals("Value cannot be empty", evt.getMessage().get()); - Assert.assertEquals(nameField, evt.getSource()); + Assert.assertNotNull(statusCapture.get()); + ValidationStatus evt = statusCapture.get(); + Assert.assertEquals(Status.ERROR, evt.getStatus()); + Assert.assertEquals(EMPTY_ERROR_MESSAGE, evt.getMessage().get()); + Assert.assertEquals(nameField, evt.getField()); nameField.setValue("foo"); - event.set(null); + statusCapture.set(null); // Second validation succeeds => should be event with OK status and // no message binder.validate(); - evt = event.get(); + evt = statusCapture.get(); Assert.assertNotNull(evt); - Assert.assertEquals(ValidationStatus.OK, evt.getStatus()); + Assert.assertEquals(Status.OK, evt.getStatus()); Assert.assertFalse(evt.getMessage().isPresent()); - Assert.assertEquals(nameField, evt.getSource()); + Assert.assertEquals(nameField, evt.getField()); } @Test public void bindingWithStatusChangeHandler_defaultStatusChangeHandlerIsReplaced() { Binding binding = binder.forField(nameField) - .withValidator(notEmpty).withStatusChangeHandler(evt -> { + .withValidator(notEmpty).withStatusHandler(evt -> { }); binding.bind(Person::getFirstName, Person::setFirstName); @@ -639,7 +644,7 @@ public class BinderTest { binding.validate(); Assert.assertTrue(label.isVisible()); - Assert.assertEquals("Value cannot be empty", label.getValue()); + Assert.assertEquals(EMPTY_ERROR_MESSAGE, label.getValue()); nameField.setValue("foo"); @@ -677,7 +682,7 @@ public class BinderTest { .withValidator(notEmpty); binding.bind(Person::getFirstName, Person::setFirstName); - binding.withStatusChangeHandler(evt -> Assert.fail()); + binding.withStatusHandler(evt -> Assert.fail()); } @Test(expected = IllegalStateException.class) @@ -697,7 +702,7 @@ public class BinderTest { Binding binding = binder.forField(nameField); - binding.withStatusChangeHandler(event -> { + binding.withStatusHandler(event -> { }); binding.withStatusLabel(label); @@ -711,7 +716,7 @@ public class BinderTest { binding.withStatusLabel(label); - binding.withStatusChangeHandler(event -> { + binding.withStatusHandler(event -> { }); } @@ -720,10 +725,10 @@ public class BinderTest { Binding binding = binder.forField(nameField); - binding.withStatusChangeHandler(event -> { + binding.withStatusHandler(event -> { }); - binding.withStatusChangeHandler(event -> { + binding.withStatusHandler(event -> { }); } @@ -738,9 +743,9 @@ public class BinderTest { Person person = new Person(); binder.bind(person); - List> errors = binder.validate(); - Assert.assertEquals(1, errors.size()); - Assert.assertFalse(errors.get(0).getField().isPresent()); + List> errors = binder.validate() + .getFieldValidationErrors(); + Assert.assertEquals(0, errors.size()); } @Test @@ -756,12 +761,12 @@ public class BinderTest { Person person = new Person(); binder.bind(person); - List> errors = binder.validate(); + List> errors = binder.validate() + .getFieldValidationErrors(); Assert.assertEquals(1, errors.size()); - ValidationError error = errors.get(0); - Assert.assertEquals(msg, error.getMessage()); - Assert.assertTrue(error.getField().isPresent()); - Assert.assertEquals(nameField.getValue(), error.getValue()); + ValidationStatus error = errors.get(0); + Assert.assertEquals(msg, error.getMessage().get()); + Assert.assertEquals(nameField, error.getField()); } @Test @@ -778,14 +783,14 @@ public class BinderTest { Person person = new Person(); binder.bind(person); - List> errors = binder.validate(); + List> errors = binder.validate() + .getFieldValidationErrors(); Assert.assertEquals(1, errors.size()); - ValidationError error = errors.get(0); + ValidationStatus error = errors.get(0); - Assert.assertEquals(msg1, error.getMessage()); - Assert.assertEquals(nameField, error.getField().get()); - Assert.assertEquals(nameField.getValue(), error.getValue()); + Assert.assertEquals(msg1, error.getMessage().get()); + Assert.assertEquals(nameField, error.getField()); } @Test @@ -799,8 +804,8 @@ public class BinderTest { Person person = new Person(); binder.bind(person); - List> errors = binder.validate(); - Assert.assertEquals(0, errors.size()); + Assert.assertFalse(binder.validate().hasErrors()); + Assert.assertTrue(binder.validate().isOk()); } @Test @@ -863,15 +868,105 @@ public class BinderTest { } @Test - public void binderWithStatusChangeHandler_handlerGetsEvents() { - AtomicReference>> resultsCapture = new AtomicReference<>(); + public void binderWithStatusHandler_fieldValidationNoBeanValidation_handlerGetsStatusUpdates() { + AtomicReference> statusCapture = new AtomicReference<>(); binder.forField(nameField).withValidator(notEmpty) - .withStatusChangeHandler(evt -> { + .withStatusHandler(evt -> { Assert.fail( "Using a custom status change handler so no change should end up here"); }).bind(Person::getFirstName, Person::setFirstName); binder.forField(ageField).withConverter(stringToInteger) - .withValidator(notNegative).withStatusChangeHandler(evt -> { + .withValidator(notNegative).withStatusHandler(evt -> { + Assert.fail( + "Using a custom status change handler so no change should end up here"); + }).bind(Person::getAge, Person::setAge); + + binder.setStatusHandler(r -> { + statusCapture.set(r); + }); + binder.bind(p); + Assert.assertNull(nameField.getComponentError()); + + nameField.setValue(""); + ageField.setValue("5"); + + // First binding validation fails => should be result with ERROR status + // and message + BinderValidationStatus status2 = binder.validate(); + BinderValidationStatus status = statusCapture.get(); + Assert.assertSame(status2, status); + + Assert.assertNull(nameField.getComponentError()); + + List> bindingStatuses = status + .getFieldValidationStatuses(); + Assert.assertNotNull(bindingStatuses); + Assert.assertEquals(1, status.getFieldValidationErrors().size()); + Assert.assertEquals(2, bindingStatuses.size()); + + ValidationStatus r = bindingStatuses.get(0); + Assert.assertTrue(r.isError()); + Assert.assertEquals(EMPTY_ERROR_MESSAGE, r.getMessage().get()); + Assert.assertEquals(nameField, r.getField()); + + r = bindingStatuses.get(1); + Assert.assertFalse(r.isError()); + Assert.assertFalse(r.getMessage().isPresent()); + Assert.assertEquals(ageField, r.getField()); + + Assert.assertEquals(0, status.getBeanValidationResults().size()); + Assert.assertEquals(0, status.getBeanValidationErrors().size()); + + nameField.setValue("foo"); + ageField.setValue(""); + + statusCapture.set(null); + // Second validation succeeds => should be result with OK status and + // no message, and error result for age + binder.validate(); + + status = statusCapture.get(); + bindingStatuses = status.getFieldValidationStatuses(); + Assert.assertEquals(1, status.getFieldValidationErrors().size()); + Assert.assertEquals(2, bindingStatuses.size()); + + r = bindingStatuses.get(0); + Assert.assertFalse(r.isError()); + Assert.assertFalse(r.getMessage().isPresent()); + Assert.assertEquals(nameField, r.getField()); + + r = bindingStatuses.get(1); + Assert.assertTrue(r.isError()); + Assert.assertEquals("Value must be a number", r.getMessage().get()); + Assert.assertEquals(ageField, r.getField()); + + Assert.assertEquals(0, status.getBeanValidationResults().size()); + Assert.assertEquals(0, status.getBeanValidationErrors().size()); + + statusCapture.set(null); + // binding validations pass, binder validation fails + ageField.setValue("0"); + binder.validate(); + + status = statusCapture.get(); + bindingStatuses = status.getFieldValidationStatuses(); + Assert.assertEquals(0, status.getFieldValidationErrors().size()); + Assert.assertEquals(2, bindingStatuses.size()); + + Assert.assertEquals(0, status.getBeanValidationResults().size()); + Assert.assertEquals(0, status.getBeanValidationErrors().size()); + } + + @Test + public void binderWithStatusHandler_fieldAndBeanLevelValidation_handlerGetsStatusUpdates() { + AtomicReference> statusCapture = new AtomicReference<>(); + binder.forField(nameField).withValidator(notEmpty) + .withStatusHandler(evt -> { + Assert.fail( + "Using a custom status change handler so no change should end up here"); + }).bind(Person::getFirstName, Person::setFirstName); + binder.forField(ageField).withConverter(stringToInteger) + .withValidator(notNegative).withStatusHandler(evt -> { Assert.fail( "Using a custom status change handler so no change should end up here"); }).bind(Person::getAge, Person::setAge); @@ -881,7 +976,7 @@ public class BinderTest { : Result.error("Need first name and age")); binder.setStatusHandler(r -> { - resultsCapture.set(r); + statusCapture.set(r); }); binder.bind(p); Assert.assertNull(nameField.getComponentError()); @@ -891,65 +986,78 @@ public class BinderTest { // First binding validation fails => should be result with ERROR status // and message - binder.validate(); + BinderValidationStatus status2 = binder.validate(); + BinderValidationStatus status = statusCapture.get(); + Assert.assertSame(status2, status); Assert.assertNull(nameField.getComponentError()); - List> results = resultsCapture.get(); - Assert.assertNotNull(results); - Assert.assertEquals(2, results.size()); + List> bindingStatuses = status + .getFieldValidationStatuses(); + Assert.assertNotNull(bindingStatuses); + Assert.assertEquals(1, status.getFieldValidationErrors().size()); + Assert.assertEquals(2, bindingStatuses.size()); - BinderResult r = results.get(0); + ValidationStatus r = bindingStatuses.get(0); Assert.assertTrue(r.isError()); - Assert.assertEquals("Value cannot be empty", r.getMessage().get()); - Assert.assertEquals(nameField, r.getField().get()); + Assert.assertEquals(EMPTY_ERROR_MESSAGE, r.getMessage().get()); + Assert.assertEquals(nameField, r.getField()); - r = results.get(1); + r = bindingStatuses.get(1); Assert.assertFalse(r.isError()); Assert.assertFalse(r.getMessage().isPresent()); - Assert.assertEquals(ageField, r.getField().get()); + Assert.assertEquals(ageField, r.getField()); + + Assert.assertEquals(0, status.getBeanValidationResults().size()); + Assert.assertEquals(0, status.getBeanValidationErrors().size()); nameField.setValue("foo"); ageField.setValue(""); - resultsCapture.set(null); + statusCapture.set(null); // Second validation succeeds => should be result with OK status and // no message, and error result for age binder.validate(); - results = resultsCapture.get(); - Assert.assertNotNull(results); - Assert.assertEquals(2, results.size()); + status = statusCapture.get(); + bindingStatuses = status.getFieldValidationStatuses(); + Assert.assertEquals(1, status.getFieldValidationErrors().size()); + Assert.assertEquals(2, bindingStatuses.size()); - r = results.get(0); + r = bindingStatuses.get(0); Assert.assertFalse(r.isError()); Assert.assertFalse(r.getMessage().isPresent()); - Assert.assertEquals(nameField, r.getField().get()); + Assert.assertEquals(nameField, r.getField()); - r = results.get(1); + r = bindingStatuses.get(1); Assert.assertTrue(r.isError()); Assert.assertEquals("Value must be a number", r.getMessage().get()); - Assert.assertEquals(ageField, r.getField().get()); + Assert.assertEquals(ageField, r.getField()); + + Assert.assertEquals(0, status.getBeanValidationResults().size()); + Assert.assertEquals(0, status.getBeanValidationErrors().size()); - resultsCapture.set(null); + statusCapture.set(null); // binding validations pass, binder validation fails ageField.setValue("0"); binder.validate(); - results = resultsCapture.get(); - Assert.assertNotNull(results); - Assert.assertEquals(1, results.size()); + status = statusCapture.get(); + bindingStatuses = status.getFieldValidationStatuses(); + Assert.assertEquals(0, status.getFieldValidationErrors().size()); + Assert.assertEquals(2, bindingStatuses.size()); - r = results.get(0); - Assert.assertTrue(r.isError()); - Assert.assertTrue(r.getMessage().isPresent()); - Assert.assertFalse(r.getField().isPresent()); + Assert.assertEquals(1, status.getBeanValidationResults().size()); + Assert.assertEquals(1, status.getBeanValidationErrors().size()); + + Assert.assertEquals("Need first name and age", + status.getBeanValidationErrors().get(0).getMessage().get()); } @Test public void binderWithStatusChangeHandler_defaultStatusChangeHandlerIsReplaced() { Binding binding = binder.forField(nameField) - .withValidator(notEmpty).withStatusChangeHandler(evt -> { + .withValidator(notEmpty).withStatusHandler(evt -> { }); binding.bind(Person::getFirstName, Person::setFirstName); @@ -991,7 +1099,7 @@ public class BinderTest { .withValidator(notEmpty); binding.bind(Person::getFirstName, Person::setFirstName); - binding.withStatusChangeHandler(evt -> Assert.fail()); + binding.withStatusHandler(evt -> Assert.fail()); } @Test(expected = IllegalStateException.class) @@ -1038,7 +1146,7 @@ public class BinderTest { @Test public void binderWithStatusChangeHandler_replaceHandler() { - AtomicReference>> capture = new AtomicReference<>(); + AtomicReference> capture = new AtomicReference<>(); Binding binding = binder.forField(nameField); binding.bind(Person::getFirstName, Person::setFirstName); @@ -1054,9 +1162,11 @@ public class BinderTest { nameField.setValue("foo"); binder.validate(); - List> results = capture.get(); + List> results = capture.get() + .getFieldValidationStatuses(); Assert.assertNotNull(results); Assert.assertEquals(1, results.size()); + Assert.assertFalse(results.get(0).isError()); } @Test -- 2.39.5