diff --git a/documentation/datamodel/datamodel-forms.asciidoc b/documentation/datamodel/datamodel-forms.asciidoc index 62fd9df38cc..62d844dee65 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 ff7e3296608..cbabdb80cb0 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 @@ protected static class BeanBindingImpl protected BeanBindingImpl(BeanBinder binder, HasValue field, Converter converter, - StatusChangeHandler statusChangeHandler) { + ValidationStatusHandler statusChangeHandler) { super(binder, field, converter, statusChangeHandler); } @@ -318,7 +318,7 @@ public BeanBinder withValidator(Validator validator) { @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 af5f4292528..c282a89e773 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 default Binding withConverter( * 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 default Binding withConverter( */ 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 default Binding withStatusLabel( * 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 @@ protected static class BindingImpl private final HasValue field; private Registration onValueChange; - private StatusChangeHandler statusChangeHandler; + private ValidationStatusHandler statusHandler; private boolean isStatusHandlerChanged; private Function getter; @@ -414,11 +413,11 @@ protected static class BindingImpl */ 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 Binding withConverter( 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 Binding withStatusChangeHandler( "A StatusChangeHandler has already been set"); } isStatusHandlerChanged = true; - statusChangeHandler = handler; + statusHandler = handler; return this; } @@ -500,33 +498,31 @@ protected void checkUnbound() { 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 @@ private FIELDVALUE convertDataToFieldType(BEAN bean) { ((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 void load(BEAN bean) { * 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 void save(BEAN bean) throws ValidationException { * updated, {@code false} otherwise */ public boolean saveIfValid(BEAN bean) { - return doSaveIfValid(bean).isEmpty(); + return doSaveIfValid(bean).isOk(); } /** @@ -845,13 +852,15 @@ public boolean saveIfValid(BEAN bean) { * @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 @@ private List> doSaveIfValid(BEAN bean) { 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 @@ private List> doSaveIfValid(BEAN bean) { // Save successful, reset hasChanges to false setHasChanges(false); } - return itemValidatorErrors; + return new BinderValidationStatus<>(this, bindingStatuses, + binderResults); } /** @@ -894,68 +905,56 @@ public Binder withValidator(Validator validator) { } /** - * 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 @@ private List> validateBindings() { * @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 Optional

@@ -1136,23 +1135,21 @@ protected void handleValidationStatusChange( * 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 52375b88ff3..00000000000 --- 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 4c516a5ed69..5da51061761 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 @@ * * @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 00000000000..f23d17e5b18 --- /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 00000000000..9528c5a884e --- /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 1555ecad2cd..00000000000 --- 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 41017a83939..68f7ad9e338 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 03f76c51273..7203655e7b8 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 e0213983415..00000000000 --- 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 790ac74fb19..932fe64575c 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.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 1788c3a9e86..84a414edf10 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 void fieldWithInvalidConverterBound_bindBean_fieldValueUpdated() { } 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 6861903132b..d11a0ed6da7 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.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 void setUp() { 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 void simpleEmailValidator() { .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 void nameLengthTest() { .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 void chainedEmailValidator() { .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 void crossFieldValidation_validateUsingBinder() { 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 void crossFieldValidation_validateUsingBinding() { 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 void withStatusLabelExample() { @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 void withBindingStatusChangeHandlerExample() { 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 void manyConvertersAndValidators() throws ValidationException { .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 void bindUsingCustomConverter() { 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 void withBinderStatusLabelExample() { // 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 void withBinderStatusChangeHandlerExample() { 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 void withBinderStatusChangeHandlerExample() { // 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 4b69fdf50e6..0a82b527bbf 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.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 @@ 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 void setStatus(String status) { 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 void save_validationErrors_exceptionContainsErrors() 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 void load_unbound_noChanges() { 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 void bound_validatorsAreOK_noErrors() { 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 Result apply(String value) { 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 void convertToModelNegativeAgeFailsOnFirstValidator() { 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 void convertInvalidDataToField() { @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 void bindingWithStatusChangeHandler_handlerGetsEvents() { // 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 void bindingWithStatusLabel_labelIsUpdatedAccordingStatus() { 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 void bindingWithStatusChangeHandler_addAfterBound() { .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 void bindingWithStatusLabel_setAfterHandler() { Binding binding = binder.forField(nameField); - binding.withStatusChangeHandler(event -> { + binding.withStatusHandler(event -> { }); binding.withStatusLabel(label); @@ -711,7 +716,7 @@ public void bindingWithStatusChangeHandler_setAfterLabel() { binding.withStatusLabel(label); - binding.withStatusChangeHandler(event -> { + binding.withStatusHandler(event -> { }); } @@ -720,10 +725,10 @@ public void bingingWithStatusChangeHandler_setAfterOtherHandler() { Binding binding = binder.forField(nameField); - binding.withStatusChangeHandler(event -> { + binding.withStatusHandler(event -> { }); - binding.withStatusChangeHandler(event -> { + binding.withStatusHandler(event -> { }); } @@ -738,9 +743,9 @@ public void validate_failedBeanValidatorWithoutFieldValidators() { 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 void validate_failedBeanValidatorWithFieldValidator() { 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 void validate_failedBothBeanValidatorAndFieldValidator() { 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 void validate_okBeanValidatorWithoutFieldValidators() { 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 void updateBoundField_bindingValdationSuccess_beanLevelValidationIsRun() } @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 void binderWithStatusChangeHandler_handlerGetsEvents() { : 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 void binderWithStatusChangeHandler_handlerGetsEvents() { // 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 void binderWithStatusChangeHandler_addAfterBound() { .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 void binderWithNullStatusChangeHandler_throws() { @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 void binderWithStatusChangeHandler_replaceHandler() { 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