]> source.dussan.org Git - vaadin-framework.git/commitdiff
Refactor Binder Status Handling API
authorPekka Hyvönen <pekka@vaadin.com>
Mon, 12 Sep 2016 09:05:19 +0000 (12:05 +0300)
committerVaadin Code Review <review@vaadin.com>
Mon, 12 Sep 2016 13:07:22 +0000 (13:07 +0000)
BinderStatusHandler is now triggered only once per validation.
Unified ValidationError and BinderResult into BinderValidationStatus.
Renamed ValidationStatusChangeEvent into ValidationStatus.
Unified handler names for validation status.

Next patch will fix resetting of field errors on reset.

Change-Id: I9536d554d781fe599fbd7e5bcb5a9ffebe675ca0

16 files changed:
documentation/datamodel/datamodel-forms.asciidoc
server/src/main/java/com/vaadin/data/BeanBinder.java
server/src/main/java/com/vaadin/data/Binder.java
server/src/main/java/com/vaadin/data/BinderResult.java [deleted file]
server/src/main/java/com/vaadin/data/BinderStatusHandler.java
server/src/main/java/com/vaadin/data/BinderValidationStatus.java [new file with mode: 0644]
server/src/main/java/com/vaadin/data/BinderValidationStatusHandler.java [new file with mode: 0644]
server/src/main/java/com/vaadin/data/StatusChangeHandler.java [deleted file]
server/src/main/java/com/vaadin/data/ValidationError.java [deleted file]
server/src/main/java/com/vaadin/data/ValidationException.java
server/src/main/java/com/vaadin/data/ValidationStatus.java
server/src/main/java/com/vaadin/data/ValidationStatusChangeEvent.java [deleted file]
server/src/main/java/com/vaadin/data/ValidationStatusHandler.java [new file with mode: 0644]
server/src/test/java/com/vaadin/data/BeanBinderTest.java
server/src/test/java/com/vaadin/data/BinderBookOfVaadinTest.java
server/src/test/java/com/vaadin/data/BinderTest.java

index 62fd9df38cca255913e5007debe5789882a5b8d7..62d844dee659518960549489066b5684e77a255d 100644 (file)
@@ -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);
 });
 ----
 
index ff7e3296608450d9d8e32f85c5bb56a5adead709..cbabdb80cb02f11b2c7f5019532980c79f3cd133 100644 (file)
@@ -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);
index af5f4292528844c48b613cdb0e980090da4f9025..c282a89e7736af8a3c6653d372cee923969748fa 100644 (file)
@@ -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() {
@@ -551,45 +547,55 @@ public class Binder<BEAN> implements Serializable {
                     ((AbstractComponent) getField()).getLocale());
         }
 
+        /**
+         * Handles the value change triggered by the bound field.
+         *
+         * @param bean
+         *            the new value
+         */
+        private void handleFieldValueChange(BEAN bean) {
+            binder.setHasChanges(true);
+            // store field value if valid
+            ValidationStatus<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,18 +1108,26 @@ 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>
@@ -1136,23 +1135,21 @@ public class Binder<BEAN> implements Serializable {
      * other status changes are displayed in the status label, if one has been
      * set with {@link #setStatusLabel(Label)}.
      *
-     * @param results
-     *            a list of validation results from binding and/or item level
+     * @param binderStatus
+     *            status of validation results from binding and/or item level
      *            validators
      */
-    @SuppressWarnings("unchecked")
     protected void defaultHandleBinderStatusChange(
-            List<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 (file)
index 52375b8..0000000
+++ /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
index 4c516a5ed69e7a30ea1f71f33834fa3ba18181e6..5da5106176145e44c68bb20bd2b432807534fccf 100644 (file)
@@ -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 (file)
index 0000000..f23d17e
--- /dev/null
@@ -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 (file)
index 0000000..9528c5a
--- /dev/null
@@ -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/StatusChangeHandler.java b/server/src/main/java/com/vaadin/data/StatusChangeHandler.java
deleted file mode 100644 (file)
index 790ac74..0000000
+++ /dev/null
@@ -1,40 +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.function.Consumer;
-
-import com.vaadin.data.Binder.Binding;
-
-/**
- * Validation status change handler.
- * <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
- *
- * @author Vaadin Ltd
- * @since 8.0
- *
- */
-public interface StatusChangeHandler
-        extends Consumer<ValidationStatusChangeEvent>, 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 (file)
index 1555eca..0000000
+++ /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;
-    }
-
-}
index 41017a83939b5de50e9348efd310dcf8e7cee73c..68f7ad9e3381ee025ec0ffb483ed123f85709cd8 100644 (file)
  */
 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;
+    }
 }
index 03f76c51273c7c253909810a620e91863633af3f..7203655e7b85e2ce5d60aa204b93513b17e9b8e5 100644 (file)
  */
 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 (file)
index e021398..0000000
+++ /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/ValidationStatusHandler.java b/server/src/main/java/com/vaadin/data/ValidationStatusHandler.java
new file mode 100644 (file)
index 0000000..932fe64
--- /dev/null
@@ -0,0 +1,43 @@
+/*
+ * 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.data.Binder.Binding;
+import com.vaadin.ui.AbstractComponent;
+
+/**
+ * Handler for {@link ValidationStatus} changes.
+ * <p>
+ * {@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 ValidationStatusHandler
+        extends Consumer<ValidationStatus<?>>, Serializable {
+
+}
index 1788c3a9e8674578f7798837249d64182dd33732..84a414edf102908bb9c38110343ee94d728b6954 100644 (file)
@@ -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());
     }
 }
index 6861903132b017dc6f663aeb7f7812cab510b48c..d11a0ed6da7f49b37ff384d1206d0b9151df2ba7 100644 (file)
@@ -27,12 +27,14 @@ import org.junit.Before;
 import org.junit.Test;
 
 import com.vaadin.data.Binder.Binding;
+import com.vaadin.data.ValidationStatus.Status;
 import com.vaadin.data.util.converter.Converter;
 import com.vaadin.data.util.converter.StringToIntegerConverter;
 import com.vaadin.data.validator.EmailValidator;
 import com.vaadin.server.AbstractErrorMessage;
 import com.vaadin.ui.Button;
 import com.vaadin.ui.Label;
+import com.vaadin.ui.Notification;
 import com.vaadin.ui.PopupDateField;
 import com.vaadin.ui.Slider;
 import com.vaadin.ui.TextField;
@@ -139,6 +141,30 @@ public class BinderBookOfVaadinTest {
         emailField = new TextField();
     }
 
+    @Test
+    public void loadingFromBusinessObjects() {
+        // this test is just to make sure the code snippet in the book compiles
+        binder.load(new BookPerson(1969, 50000));
+
+        BinderValidationStatus<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)
@@ -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());
index 4b69fdf50e651b855685d13c38ce8adedbc3bc62..0a82b527bbf7ec60e257fafe86a8ab2699c3380c 100644 (file)
@@ -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