diff options
15 files changed, 886 insertions, 640 deletions
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<ValidationError<?>> validationErrors = binder.validate(); +ValidationStatus<Person> 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<Result<?>> 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<BEAN> extends Binder<BEAN> { protected BeanBindingImpl(BeanBinder<BEAN> binder, HasValue<FIELDVALUE> field, Converter<FIELDVALUE, TARGET> converter, - StatusChangeHandler statusChangeHandler) { + ValidationStatusHandler statusChangeHandler) { super(binder, field, converter, statusChangeHandler); } @@ -318,7 +318,7 @@ public class BeanBinder<BEAN> extends Binder<BEAN> { @Override protected <FIELDVALUE, TARGET> BeanBindingImpl<BEAN, FIELDVALUE, TARGET> createBinding( HasValue<FIELDVALUE> field, Converter<FIELDVALUE, TARGET> 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<BEAN> implements Serializable { * default behavior). * <p> * 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<BEAN> implements Serializable { */ public default Binding<BEAN, FIELDVALUE, TARGET> 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<BEAN> implements Serializable { * status change handler * @return this binding, for chaining */ - public Binding<BEAN, FIELDVALUE, TARGET> withStatusChangeHandler( - StatusChangeHandler handler); + public Binding<BEAN, FIELDVALUE, TARGET> 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<TARGET> validate(); + public ValidationStatus<TARGET> validate(); } @@ -387,7 +386,7 @@ public class Binder<BEAN> implements Serializable { private final HasValue<FIELDVALUE> field; private Registration onValueChange; - private StatusChangeHandler statusChangeHandler; + private ValidationStatusHandler statusHandler; private boolean isStatusHandlerChanged; private Function<BEAN, TARGET> getter; @@ -414,11 +413,11 @@ public class Binder<BEAN> implements Serializable { */ protected BindingImpl(Binder<BEAN> binder, HasValue<FIELDVALUE> field, Converter<FIELDVALUE, TARGET> 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<BEAN> implements Serializable { Objects.requireNonNull(converter, "converter cannot be null"); return getBinder().createBinding(getField(), - converterValidatorChain.chain(converter), - statusChangeHandler); + converterValidatorChain.chain(converter), statusHandler); } @Override - public Binding<BEAN, FIELDVALUE, TARGET> withStatusChangeHandler( - StatusChangeHandler handler) { + public Binding<BEAN, FIELDVALUE, TARGET> withStatusHandler( + ValidationStatusHandler handler) { checkUnbound(); Objects.requireNonNull(handler, "handler cannot be null"); if (isStatusHandlerChanged) { @@ -465,7 +463,7 @@ public class Binder<BEAN> implements Serializable { "A StatusChangeHandler has already been set"); } isStatusHandlerChanged = true; - statusChangeHandler = handler; + statusHandler = handler; return this; } @@ -500,33 +498,31 @@ public class Binder<BEAN> 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<TARGET> validate() { - BinderResult<FIELDVALUE, TARGET> bindingResult = getTargetValue(); - getBinder().getStatusHandler().accept(Arrays.asList(bindingResult)); - return bindingResult; + public ValidationStatus<TARGET> validate() { + ValidationStatus<TARGET> 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<FIELDVALUE, TARGET> getTargetValue() { + private ValidationStatus<TARGET> getTargetValue() { FIELDVALUE fieldValue = field.getValue(); Result<TARGET> 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() { @@ -552,44 +548,54 @@ public class Binder<BEAN> implements Serializable { } /** + * 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<TARGET> fieldValidationStatus = storeFieldValue( + bean); + List<Result<?>> 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 - * <code>true</code> to run item level validators if all - * field validators pass, <code>false</code> to always skip - * item level validators */ - private void storeFieldValue(BEAN bean, - boolean runBeanLevelValidation) { + private ValidationStatus<TARGET> storeFieldValue(BEAN bean) { assert bean != null; + + ValidationStatus<TARGET> validationStatus = getTargetValue(); if (setter != null) { - BinderResult<FIELDVALUE, TARGET> 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<BEAN> implements Serializable { * if some of the bound field values fail to validate */ public void save(BEAN bean) throws ValidationException { - List<ValidationError<?>> errors = doSaveIfValid(bean); - if (!errors.isEmpty()) { - throw new ValidationException(errors); + BinderValidationStatus<BEAN> status = doSaveIfValid(bean); + if (status.hasErrors()) { + throw new ValidationException(status.getFieldValidationErrors(), + status.getBeanValidationErrors()); } } @@ -833,7 +840,7 @@ public class Binder<BEAN> 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<BEAN> implements Serializable { * @return a list of field validation errors if such occur, otherwise a list * of bean validation errors. */ - private List<ValidationError<?>> doSaveIfValid(BEAN bean) { + private BinderValidationStatus<BEAN> doSaveIfValid(BEAN bean) { Objects.requireNonNull(bean, "bean cannot be null"); // First run fields level validation - List<ValidationError<?>> errors = validateBindings(); + List<ValidationStatus<?>> 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<BEAN> 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<ValidationError<?>> itemValidatorErrors = validateItem(bean); - if (!itemValidatorErrors.isEmpty()) { + List<Result<?>> 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<BEAN> 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<BEAN> 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. * <p> * 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. * <p> - * 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<ValidationError<?>> validate() { - List<ValidationError<?>> errors = validateBindings(); - if (!errors.isEmpty()) { - return errors; - } - - if (bean != null) { - return validateItem(bean); + public BinderValidationStatus<BEAN> validate() { + List<ValidationStatus<?>> bindingStatuses = validateBindings(); + + BinderValidationStatus<BEAN> 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. - * <p> - * If all validators pass, the resulting list is empty. + * of validation statuses. * <p> * Does not run bean validators. - * <p> - * 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<ValidationError<?>> validateBindings() { - List<BinderResult<?, ?>> results = new ArrayList<>(); + private List<ValidationStatus<?>> validateBindings() { + List<ValidationStatus<?>> 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. * <p> - * If all validators pass, the resulting list is empty. * * @see #withValidator(Validator) * @@ -964,20 +963,12 @@ public class Binder<BEAN> implements Serializable { * @return a list of validation errors or an empty list if validation * succeeded */ - private List<ValidationError<?>> validateItem(BEAN bean) { + private List<Result<?>> validateItem(BEAN bean) { Objects.requireNonNull(bean, "bean cannot be null"); - List<BinderResult<?, ?>> results = Collections.unmodifiableList( + List<Result<?>> 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<BEAN> implements Serializable { * @throws NullPointerException * for <code>null</code> 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<BEAN> implements Serializable { */ protected <FIELDVALUE, TARGET> BindingImpl<BEAN, FIELDVALUE, TARGET> createBinding( HasValue<FIELDVALUE> field, Converter<FIELDVALUE, TARGET> converter, - StatusChangeHandler handler) { + ValidationStatusHandler handler) { return new BindingImpl<>(this, field, converter, handler); } @@ -1117,42 +1108,48 @@ public class Binder<BEAN> 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<BindingImpl<BEAN, ?, ?>> getBindings() { + return bindings; + } + + /** * The default binder level status handler. * <p> * Passes all field related results to the Binding status handlers. All * 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<BinderResult<?, ?>> results) { + BinderValidationStatus<?> binderStatus) { // let field events go to binding status handlers - results.stream().filter(br -> br.getField().isPresent()) - .forEach(br -> ((BindingImpl<BEAN, ?, ?>) 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 <FIELDVALUE> - * the value type of the field - * @param <VALUE> - * 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<FIELDVALUE, VALUE> extends SimpleResult<VALUE> { - - private final Binding<?, FIELDVALUE, VALUE> 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 <code>null</code> - * @param message - * the error message of the result, may be {@code null} - */ - public BinderResult(Binding<?, FIELDVALUE, VALUE> 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<Binding<?, FIELDVALUE, VALUE>> getBinding() { - return Optional.ofNullable(binding); - } - - /** - * Return the field this result originated from, or an empty optional if - * none. - * - * @return the optional field - */ - public Optional<HasValue<FIELDVALUE>> 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<List<BinderResult<?, ?>>>, Serializable { + extends Consumer<BinderValidationStatus<?>>, 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. + * <p> + * Note: if there are any field level validation errors, the bean level + * validation is not run. + * <p> + * Use {@link Binder#setStatusHandler(BinderStatusHandler)} to handle form level + * validation status changes. + * + * @author Vaadin Ltd + * + * @param <BEAN> + * the bean type of the binder + * + * @see BinderValidationStatusHandler + * @see Binder#setStatusHandler(BinderStatusHandler) + * @see Binder#validate() + * @see ValidationStatus + * + * @since 8.0 + */ +public class BinderValidationStatus<BEAN> implements Serializable { + + private final Binder<BEAN> binder; + private final List<ValidationStatus<?>> bindingStatuses; + private final List<Result<?>> 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<BEAN> source, + List<ValidationStatus<?>> bindingStatuses, + List<Result<?>> 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<BEAN> getBinder() { + return binder; + } + + /** + * Gets both field and bean level validation errors. + * + * @return a list of all validation errors + */ + public List<Result<?>> getValidationErrors() { + ArrayList<Result<?>> 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<ValidationStatus<?>> getFieldValidationStatuses() { + return bindingStatuses; + } + + /** + * Gets the bean level validation results. + * + * @return the bean level validation results + */ + public List<Result<?>> getBeanValidationResults() { + return binderStatuses; + } + + /** + * Gets the failed field level validation statuses. + * + * @return a list of failed field level validation statuses + */ + public List<ValidationStatus<?>> 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<Result<?>> 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. + * <p> + * {{@link Binder#setStatusHandler(BinderStatusHandler) Register} an instance of + * this class to be able to customize validation status handling. + * <p> + * 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 <BEAN> + * the bean type of the binder + * + * @see BinderValidationStatus + * @see Binder#validate() + * @see ValidationStatus + * + * @since 8.0 + */ +public interface BinderValidationStatusHandler<BEAN> + extends Consumer<BinderValidationStatus<BEAN>>, 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. - * <p> - * 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 <V> - * the value type - */ -public class ValidationError<V> implements Serializable { - - /** - * This is either a {@link Binding} or a {@link Binder}. - */ - private final Object source; - private final String message; - /** - * This is either HasValue<V> 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<HasValue<V>> 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. - * <p> - * 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<ValidationError<?>> errors; + private final List<ValidationStatus<?>> bindingValidationErrors; + private final List<Result<?>> 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<ValidationError<?>> errors) { + public ValidationException( + List<ValidationStatus<?>> bindingValidationErrors, + List<Result<?>> 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<ValidationError<?>> getValidationError() { + public List<Result<?>> getValidationErrors() { + ArrayList<Result<?>> 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<ValidationStatus<?>> 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<Result<?>> 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. - * <p> - * 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 <TARGET> + * 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<TARGET> implements Serializable { + + /** + * Status of the validation. + * <p> + * 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<TARGET> result; + private final Binding<?, ?, TARGET> 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<?, ?, TARGET> source, + Result<TARGET> result) { + this(source, result.isError() ? Status.ERROR : Status.OK, result); + } + + /** + * Creates a new status change event. + * <p> + * 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<?, ?, TARGET> source, Status status, + Result<TARGET> 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<String> 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<Result<TARGET>> getResult() { + return Optional.ofNullable(result); + } + + /** + * Gets the source binding of the validation status. + * + * @return the source binding + */ + public Binding<?, ?, TARGET> 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. - * <p> - * 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<String> 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 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. * <p> - * 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<ValidationStatusChangeEvent>, Serializable { +public interface ValidationStatusHandler + extends Consumer<ValidationStatus<?>>, 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<ValidationError<?>> errors = binder.validate(); + BinderValidationStatus<?> status = binder.validate(); + List<ValidationStatus<?>> 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; @@ -140,6 +142,30 @@ public class BinderBookOfVaadinTest { } @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<BookPerson> 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) // Explicit validator instance @@ -148,16 +174,16 @@ public class BinderBookOfVaadinTest { .bind(BookPerson::getEmail, BookPerson::setEmail); field.setValue("not-email"); - List<ValidationError<?>> 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<ValidationError<?>> 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<ValidationError<?>> 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<ValidationError<?>> errors = binder.validate(); - Assert.assertTrue(errors.isEmpty()); + BinderValidationStatus<Trip> 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<Date> result = returnBinding.validate(); + ValidationStatus<Date> 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<ValidationStatusChangeEvent> event = new AtomicReference<>(); + AtomicReference<ValidationStatus<?>> 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<ValidationError<?>> 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<Result<?>> 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<ValidationError<?>> 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<String> notEmpty = Validator.from(val -> !val.isEmpty(), - "Value cannot be empty"); + EMPTY_ERROR_MESSAGE); Converter<String, Integer> stringToInteger = Converter.from( Integer::valueOf, String::valueOf, e -> "Value must be a number"); Validator<Integer> notNegative = Validator.from(x -> x >= 0, @@ -327,18 +327,17 @@ public class BinderTest { binder.save(person); Assert.fail(); } catch (ValidationException exception) { - List<ValidationError<?>> validationErrors = exception - .getValidationError(); + List<ValidationStatus<?>> 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<Person> binder = new Binder<>(); - List<ValidationError<?>> errors = binder.validate(); + BinderValidationStatus<Person> 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<ValidationError<?>> errors = binder.validate(); + BinderValidationStatus<Person> 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<ValidationError<?>> errors = binder.validate(); + BinderValidationStatus<Person> status = binder.validate(); + List<ValidationStatus<?>> errors = status.getFieldValidationErrors(); Assert.assertEquals(1, errors.size()); - Set<String> 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<ValidationError<?>> validationErrors, + List<ValidationStatus<?>> 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<Person> 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<ValidationStatusChangeEvent> event = new AtomicReference<>(); + AtomicReference<ValidationStatus<?>> statusCapture = new AtomicReference<>(); Binding<Person, String, String> 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<Person, String, String> 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<Person, String, String> 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<Person, String, String> 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<ValidationError<?>> errors = binder.validate(); - Assert.assertEquals(1, errors.size()); - Assert.assertFalse(errors.get(0).getField().isPresent()); + List<ValidationStatus<?>> 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<ValidationError<?>> errors = binder.validate(); + List<ValidationStatus<?>> 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<ValidationError<?>> errors = binder.validate(); + List<ValidationStatus<?>> 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<ValidationError<?>> 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<List<BinderResult<?, ?>>> resultsCapture = new AtomicReference<>(); + public void binderWithStatusHandler_fieldValidationNoBeanValidation_handlerGetsStatusUpdates() { + AtomicReference<BinderValidationStatus<?>> 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<Person> status2 = binder.validate(); + BinderValidationStatus<?> status = statusCapture.get(); + Assert.assertSame(status2, status); + + Assert.assertNull(nameField.getComponentError()); + + List<ValidationStatus<?>> 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<BinderValidationStatus<?>> 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<Person> status2 = binder.validate(); + BinderValidationStatus<?> status = statusCapture.get(); + Assert.assertSame(status2, status); Assert.assertNull(nameField.getComponentError()); - List<BinderResult<?, ?>> results = resultsCapture.get(); - Assert.assertNotNull(results); - Assert.assertEquals(2, results.size()); + List<ValidationStatus<?>> 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<Person, String, String> 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<List<BinderResult<?, ?>>> capture = new AtomicReference<>(); + AtomicReference<BinderValidationStatus<?>> capture = new AtomicReference<>(); Binding<Person, String, String> binding = binder.forField(nameField); binding.bind(Person::getFirstName, Person::setFirstName); @@ -1054,9 +1162,11 @@ public class BinderTest { nameField.setValue("foo"); binder.validate(); - List<BinderResult<?, ?>> results = capture.get(); + List<ValidationStatus<?>> results = capture.get() + .getFieldValidationStatuses(); Assert.assertNotNull(results); Assert.assertEquals(1, results.size()); + Assert.assertFalse(results.get(0).isError()); } @Test |