aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--documentation/datamodel/datamodel-forms.asciidoc4
-rw-r--r--server/src/main/java/com/vaadin/data/Binder.java133
-rw-r--r--server/src/main/java/com/vaadin/data/Result.java170
-rw-r--r--server/src/main/java/com/vaadin/data/SimpleResult.java96
-rw-r--r--server/src/main/java/com/vaadin/data/ValidationError.java68
-rw-r--r--server/src/main/java/com/vaadin/data/Validator.java136
-rw-r--r--server/src/test/java/com/vaadin/data/BinderTest.java68
-rw-r--r--server/src/test/java/com/vaadin/data/ResultTest.java105
-rw-r--r--server/src/test/java/com/vaadin/data/ValidatorTest.java93
9 files changed, 868 insertions, 5 deletions
diff --git a/documentation/datamodel/datamodel-forms.asciidoc b/documentation/datamodel/datamodel-forms.asciidoc
index cbb50d1743..06705a7fc6 100644
--- a/documentation/datamodel/datamodel-forms.asciidoc
+++ b/documentation/datamodel/datamodel-forms.asciidoc
@@ -322,7 +322,7 @@ 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
-Set<BinderResult> validationErrors = binder.validate();
+List<ValidationError<?>> validationErrors = binder.validate();
if (!validationErrors.isEmpty()) {
Notification.show("Validation error count: "
@@ -594,7 +594,7 @@ Button saveButton = new Button("Save", event -> {
// Create non-shared copy to use for validation
Person copy = new Person(person);
- Set<BinderResult> errors = binder.validateWithBean(copy);
+ List<ValidationError<?>> errors = binder.validateWithBean(copy);
if (errors.isEmpty()) {
// Write new values to the actual bean
diff --git a/server/src/main/java/com/vaadin/data/Binder.java b/server/src/main/java/com/vaadin/data/Binder.java
index 3ae5a82e3e..a34db33e51 100644
--- a/server/src/main/java/com/vaadin/data/Binder.java
+++ b/server/src/main/java/com/vaadin/data/Binder.java
@@ -16,14 +16,20 @@
package com.vaadin.data;
import java.io.Serializable;
+import java.util.ArrayList;
import java.util.LinkedHashSet;
+import java.util.List;
import java.util.Objects;
import java.util.Optional;
import java.util.Set;
import java.util.function.BiConsumer;
import java.util.function.Function;
+import java.util.function.Predicate;
+import java.util.stream.Collectors;
import com.vaadin.event.Registration;
+import com.vaadin.server.UserError;
+import com.vaadin.ui.AbstractComponent;
/**
* Connects one or more {@code Field} components to properties of a backing data
@@ -105,6 +111,42 @@ public class Binder<T> implements Serializable {
* if {@code bind} has already been called on this binding
*/
public void bind(Function<T, V> getter, BiConsumer<T, V> setter);
+
+ /**
+ * Adds a validator to this binding. Validators are applied, in
+ * registration order, when the field value is saved to the backing
+ * property. If any validator returns a failure, the property value is
+ * not updated.
+ *
+ * @param validator
+ * the validator to add, not null
+ * @return this binding, for chaining
+ * @throws IllegalStateException
+ * if {@code bind} has already been called
+ */
+ public Binding<T, V> withValidator(Validator<? super V> validator);
+
+ /**
+ * A convenience method to add a validator to this binding using the
+ * {@link Validator#from(Predicate, String)} factory method.
+ * <p>
+ * Validators are applied, in registration order, when the field value
+ * is saved to the backing property. If any validator returns a failure,
+ * the property value is not updated.
+ *
+ * @see #withValidator(Validator)
+ * @see Validator#from(Predicate, String)
+ *
+ * @param predicate
+ * the predicate performing validation, not null
+ * @param message
+ * the error message to report in case validation failure
+ * @return this binding, for chaining
+ * @throws IllegalStateException
+ * if {@code bind} has already been called
+ */
+ public Binding<T, V> withValidator(Predicate<? super V> predicate,
+ String message);
}
/**
@@ -121,6 +163,8 @@ public class Binder<T> implements Serializable {
private Function<T, V> getter;
private BiConsumer<T, V> setter;
+ private List<Validator<? super V>> validators = new ArrayList<>();
+
/**
* Creates a new binding associated with the given field.
*
@@ -144,12 +188,35 @@ public class Binder<T> implements Serializable {
}
}
+ @Override
+ public Binding<T, V> withValidator(Validator<? super V> validator) {
+ checkUnbound();
+ Objects.requireNonNull(validator, "validator cannot be null");
+ validators.add(validator);
+ return this;
+ }
+
+ @Override
+ public Binding<T, V> withValidator(Predicate<? super V> predicate,
+ String message) {
+ return withValidator(Validator.from(predicate, message));
+ }
+
private void bind(T bean) {
setFieldValue(bean);
onValueChange = field
.addValueChangeListener(e -> storeFieldValue(bean));
}
+ private List<ValidationError<V>> validate() {
+ return validators.stream()
+ .map(validator -> validator.apply(field.getValue()))
+ .filter(Result::isError)
+ .map(result -> new ValidationError<>(field,
+ result.getMessage().orElse(null)))
+ .collect(Collectors.toList());
+ }
+
private void unbind() {
onValueChange.remove();
}
@@ -186,6 +253,7 @@ public class Binder<T> implements Serializable {
"cannot modify binding: already bound to a property");
}
}
+
}
private T bean;
@@ -287,13 +355,34 @@ public class Binder<T> implements Serializable {
}
/**
+ * Validates the values of all bound fields and returns the result of the
+ * validation as a set of validation errors.
+ * <p>
+ * Validation is successful if the resulting set is empty.
+ *
+ * @return the validation result.
+ */
+ public List<ValidationError<?>> validate() {
+ List<ValidationError<?>> resultErrors = new ArrayList<>();
+ for (BindingImpl<?> binding : bindings) {
+ clearError(binding.field);
+ List<? extends ValidationError<?>> errors = binding.validate();
+ resultErrors.addAll(errors);
+ if (!errors.isEmpty()) {
+ handleError(binding.field, errors.get(0).getMessage());
+ }
+ }
+ return resultErrors;
+ }
+
+ /**
* Unbinds the currently bound bean if any. If there is no bound bean, does
* nothing.
*/
public void unbind() {
if (bean != null) {
bean = null;
- bindings.forEach(b -> b.unbind());
+ bindings.forEach(BindingImpl::unbind);
}
}
@@ -308,7 +397,10 @@ public class Binder<T> implements Serializable {
*/
public void load(T bean) {
Objects.requireNonNull(bean, "bean cannot be null");
- bindings.forEach(binding -> binding.setFieldValue(bean));
+ bindings.forEach(
+
+ binding -> binding.setFieldValue(bean));
+
}
/**
@@ -323,7 +415,10 @@ public class Binder<T> implements Serializable {
*/
public void save(T bean) {
Objects.requireNonNull(bean, "bean cannot be null");
- bindings.forEach(binding -> binding.storeFieldValue(bean));
+ bindings.forEach(
+
+ binding -> binding.storeFieldValue(bean));
+
}
/**
@@ -341,4 +436,36 @@ public class Binder<T> implements Serializable {
return b;
}
+ /**
+ * Clears the error condition of the given field, if any. The default
+ * implementation clears the
+ * {@link AbstractComponent#setComponentError(ErrorMessage) component error}
+ * of the field if it is a Component, otherwise does nothing.
+ *
+ * @param field
+ * the field with an invalid value
+ */
+ protected void clearError(HasValue<?> field) {
+ if (field instanceof AbstractComponent) {
+ ((AbstractComponent) field).setComponentError(null);
+ }
+ }
+
+ /**
+ * Handles a validation error emitted when trying to save the value of the
+ * given field. The default implementation sets the
+ * {@link AbstractComponent#setComponentError(ErrorMessage) component error}
+ * of the field if it is a Component, otherwise does nothing.
+ *
+ * @param field
+ * the field with the invalid value
+ * @param error
+ * the error message to set
+ */
+ protected void handleError(HasValue<?> field, String error) {
+ if (field instanceof AbstractComponent) {
+ ((AbstractComponent) field).setComponentError(new UserError(error));
+ }
+ }
+
}
diff --git a/server/src/main/java/com/vaadin/data/Result.java b/server/src/main/java/com/vaadin/data/Result.java
new file mode 100644
index 0000000000..0d6ffad94e
--- /dev/null
+++ b/server/src/main/java/com/vaadin/data/Result.java
@@ -0,0 +1,170 @@
+/*
+ * Copyright 2000-2014 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 java.util.function.Consumer;
+import java.util.function.Function;
+import java.util.function.Supplier;
+
+/**
+ * Represents the result of an operation that might fail, such as input
+ * validation or type conversion. A result may contain either a value,
+ * signifying a successful operation, or an error message in case of a failure.
+ * <p>
+ * Result instances are created using the factory methods {@link #ok(R)} and
+ * {@link #error(String)}, denoting success and failure respectively.
+ * <p>
+ * Unless otherwise specified, {@code Result} method arguments cannot be null.
+ *
+ * @param <R>
+ * the result value type
+ */
+public interface Result<R> extends Serializable {
+
+ /**
+ * Returns a successful result wrapping the given value.
+ *
+ * @param <R>
+ * the result value type
+ * @param value
+ * the result value, can be null
+ * @return a successful result
+ */
+ public static <R> Result<R> ok(R value) {
+ return new SimpleResult<>(value, null);
+ }
+
+ /**
+ * Returns a failure result wrapping the given error message.
+ *
+ * @param <R>
+ * the result value type
+ * @param message
+ * the error message
+ * @return a failure result
+ */
+ public static <R> Result<R> error(String message) {
+ Objects.requireNonNull(message, "message cannot be null");
+ return new SimpleResult<R>(null, message);
+ }
+
+ /**
+ * Returns a Result representing the result of invoking the given supplier.
+ * If the supplier returns a value, returns a {@code Result.ok} of the
+ * value; if an exception is thrown, returns the message in a
+ * {@code Result.error}.
+ *
+ * @param <R>
+ * the result value type
+ * @param supplier
+ * the supplier to run
+ * @param onError
+ * the function to provide the error message
+ * @return the result of invoking the supplier
+ */
+ public static <R> Result<R> of(Supplier<R> supplier,
+ Function<Exception, String> onError) {
+ Objects.requireNonNull(supplier, "supplier cannot be null");
+ Objects.requireNonNull(onError, "onError cannot be null");
+
+ try {
+ return ok(supplier.get());
+ } catch (Exception e) {
+ return error(onError.apply(e));
+ }
+ }
+
+ /**
+ * If this Result has a value, returns a Result of applying the given
+ * function to the value. Otherwise, returns a Result bearing the same error
+ * as this one. Note that any exceptions thrown by the mapping function are
+ * not wrapped but allowed to propagate.
+ *
+ * @param <S>
+ * the type of the mapped value
+ * @param mapper
+ * the mapping function
+ * @return the mapped result
+ */
+ public default <S> Result<S> map(Function<R, S> mapper) {
+ return flatMap(value -> ok(mapper.apply(value)));
+ }
+
+ /**
+ * If this Result has a value, applies the given Result-returning function
+ * to the value. Otherwise, returns a Result bearing the same error as this
+ * one. Note that any exceptions thrown by the mapping function are not
+ * wrapped but allowed to propagate.
+ *
+ * @param <S>
+ * the type of the mapped value
+ * @param mapper
+ * the mapping function
+ * @return the mapped result
+ */
+ public <S> Result<S> flatMap(Function<R, Result<S>> mapper);
+
+ /**
+ * Invokes either the first callback or the second one, depending on whether
+ * this Result denotes a success or a failure, respectively.
+ *
+ * @param ifOk
+ * the function to call if success
+ * @param ifError
+ * the function to call if failure
+ */
+ public void handle(Consumer<R> ifOk, Consumer<String> ifError);
+
+ /**
+ * Applies the {@code consumer} if result is not an error.
+ *
+ * @param consumer
+ * consumer to apply in case it's not an error
+ */
+ public default void ifOk(Consumer<R> consumer) {
+ handle(consumer, error -> {
+ });
+ }
+
+ /**
+ * Applies the {@code consumer} if result is an error.
+ *
+ * @param consumer
+ * consumer to apply in case it's an error
+ */
+ public default void ifError(Consumer<String> consumer) {
+ handle(value -> {
+ }, consumer);
+ }
+
+ /**
+ * Returns {@code true} if result is an error.
+ *
+ * @return whether the result is an error
+ */
+ public boolean isError();
+
+ /**
+ * Returns an Optional of the result message, or an empty Optional if none.
+ *
+ * @return the optional message
+ */
+ public Optional<String> getMessage();
+}
diff --git a/server/src/main/java/com/vaadin/data/SimpleResult.java b/server/src/main/java/com/vaadin/data/SimpleResult.java
new file mode 100644
index 0000000000..75e9cbef12
--- /dev/null
+++ b/server/src/main/java/com/vaadin/data/SimpleResult.java
@@ -0,0 +1,96 @@
+/*
+ * Copyright 2000-2014 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.Objects;
+import java.util.Optional;
+import java.util.function.Consumer;
+import java.util.function.Function;
+
+/**
+ * An internal implementation of {@code Result}.
+ *
+ * @param <R>
+ * the result value type
+ */
+class SimpleResult<R> implements Result<R> {
+
+ private final R value;
+ private final String message;
+
+ /**
+ * Creates a new {@link Result} instance using {@code value} for a non error
+ * {@link Result} and {@code message} for an error {@link Result}.
+ * <p>
+ * If {@code message} is null then {@code value} is ignored and result is an
+ * error.
+ *
+ * @param value
+ * the value of the result, may be {@code null}
+ * @param message
+ * the error message of the result, may be {@code null}
+ */
+ SimpleResult(R value, String message) {
+ // value != null => message == null
+ assert value == null
+ || message == null : "Message must be null if value is provided";
+ this.value = value;
+ this.message = message;
+ }
+
+ @Override
+ @SuppressWarnings("unchecked")
+ public <S> Result<S> flatMap(Function<R, Result<S>> mapper) {
+ Objects.requireNonNull(mapper, "mapper cannot be null");
+
+ if (isError()) {
+ // Safe cast; valueless
+ return (Result<S>) this;
+ } else {
+ return mapper.apply(value);
+ }
+ }
+
+ @Override
+ public void handle(Consumer<R> ifOk, Consumer<String> ifError) {
+ Objects.requireNonNull(ifOk, "ifOk cannot be null");
+ Objects.requireNonNull(ifError, "ifError cannot be null");
+ if (isError()) {
+ ifError.accept(message);
+ } else {
+ ifOk.accept(value);
+ }
+ }
+
+ @Override
+ public Optional<String> getMessage() {
+ return Optional.ofNullable(message);
+ }
+
+ @Override
+ public boolean isError() {
+ return message != null;
+ }
+
+ @Override
+ public String toString() {
+ if (isError()) {
+ return "error(" + message + ")";
+ } else {
+ return "ok(" + value + ")";
+ }
+ }
+}
diff --git a/server/src/main/java/com/vaadin/data/ValidationError.java b/server/src/main/java/com/vaadin/data/ValidationError.java
new file mode 100644
index 0000000000..1aa8d2b9a8
--- /dev/null
+++ b/server/src/main/java/com/vaadin/data/ValidationError.java
@@ -0,0 +1,68 @@
+/*
+ * Copyright 2000-2014 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;
+
+/**
+ * Represents a validation error. An error contains a reference to a field whose
+ * value is invalid and a message describing a validation failure.
+ *
+ * @author Vaadin Ltd
+ * @since 8.0
+ *
+ * @param <V>
+ * the field value type
+ */
+public class ValidationError<V> implements Serializable {
+
+ private HasValue<V> field;
+ private String message;
+
+ /**
+ * Creates a new instance of ValidationError with provided validated field
+ * and error message.
+ *
+ * @param field
+ * the validated field
+ * @param message
+ * the validation error message, not {@code null}
+ */
+ public ValidationError(HasValue<V> field, String message) {
+ Objects.requireNonNull(message, "message cannot be null");
+ this.field = field;
+ this.message = message;
+ }
+
+ /**
+ * Returns a reference to the validated field.
+ *
+ * @return the validated field
+ */
+ public HasValue<V> getField() {
+ return field;
+ }
+
+ /**
+ * Returns a validation error message.
+ *
+ * @return the validation error message
+ */
+ public String getMessage() {
+ return message;
+ }
+}
diff --git a/server/src/main/java/com/vaadin/data/Validator.java b/server/src/main/java/com/vaadin/data/Validator.java
new file mode 100644
index 0000000000..28dabca7e7
--- /dev/null
+++ b/server/src/main/java/com/vaadin/data/Validator.java
@@ -0,0 +1,136 @@
+/*
+ * Copyright 2000-2014 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.function.Function;
+import java.util.function.Predicate;
+
+/**
+ * A functional interface for validating user input or other potentially invalid
+ * data. When a validator instance is applied to a value of the corresponding
+ * type, it returns a <i>result</i> signifying that the value either passed or
+ * failed the validation.
+ * <p>
+ * For instance, the following validator checks if a number is positive:
+ *
+ * <pre>
+ * Validator&lt;Integer&gt; v = num -> {
+ * if (num >= 0)
+ * return Result.ok(num);
+ * else
+ * return Result.error("number must be positive");
+ * };
+ * </pre>
+ *
+ * @author Vaadin Ltd.
+ *
+ * @param <T>
+ * the type of the value to validate
+ *
+ * @see Result
+ */
+@FunctionalInterface
+public interface Validator<T> extends Function<T, Result<T>>, Serializable {
+
+ /**
+ * Returns a validator that chains this validator with the given function.
+ * Specifically, the function may be another validator. The resulting
+ * validator first applies this validator, and if the value passes, then the
+ * given validator.
+ * <p>
+ * For instance, the following chained validator checks if a number is
+ * between 0 and 10, inclusive:
+ *
+ * <pre>
+ * Validator&lt;Integer&gt; v = Validator.from(num -> num >= 0, "number must be >= 0")
+ * .chain(Validator.from(num -> num <= 10, "number must be <= 10"));
+ * </pre>
+ *
+ * @param next
+ * the validator to apply next, not null
+ * @return a chained validator
+ *
+ * @see #from(Predicate, String)
+ */
+ public default Validator<T> chain(Function<T, Result<T>> next) {
+ Objects.requireNonNull(next, "next cannot be null");
+ return val -> apply(val).flatMap(next);
+ }
+
+ /**
+ * Validates the given value. Returns a {@code Result} instance representing
+ * the outcome of the validation.
+ *
+ * @param value
+ * the input value to validate
+ * @return the validation result
+ */
+ @Override
+ public Result<T> apply(T value);
+
+ /**
+ * Returns a validator that passes any value.
+ *
+ * @param <T>
+ * the value type
+ * @return an always-passing validator
+ */
+ public static <T> Validator<T> alwaysPass() {
+ return v -> Result.ok(v);
+ }
+
+ /**
+ * Builds a validator out of a conditional function and an error message. If
+ * the function returns true, the validator returns {@code Result.ok()}; if
+ * it returns false or throws an exception, {@code Result.error()} is
+ * returned with the given message.
+ * <p>
+ * For instance, the following validator checks if a number is between 0 and
+ * 10, inclusive:
+ *
+ * <pre>
+ * Validator&lt;Integer&gt; v = Validator.from(num -> num >= 0 && num <= 10,
+ * "number must be between 0 and 10");
+ * </pre>
+ *
+ * @param <T>
+ * the value type
+ * @param guard
+ * the function used to validate, not null
+ * @param errorMessage
+ * the message returned if validation fails, not null
+ * @return the new validator using the function
+ */
+ public static <T> Validator<T> from(Predicate<T> guard,
+ String errorMessage) {
+ Objects.requireNonNull(guard, "guard cannot be null");
+ Objects.requireNonNull(errorMessage, "errorMessage cannot be null");
+ return value -> {
+ try {
+ if (guard.test(value)) {
+ return Result.ok(value);
+ } else {
+ return Result.error(errorMessage);
+ }
+ } catch (Exception e) {
+ return Result.error(errorMessage);
+ }
+ };
+ }
+}
diff --git a/server/src/test/java/com/vaadin/data/BinderTest.java b/server/src/test/java/com/vaadin/data/BinderTest.java
index 2e284ca292..c3193603ef 100644
--- a/server/src/test/java/com/vaadin/data/BinderTest.java
+++ b/server/src/test/java/com/vaadin/data/BinderTest.java
@@ -4,10 +4,18 @@ import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertSame;
+import java.util.List;
+import java.util.Set;
+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.server.AbstractErrorMessage;
+import com.vaadin.server.ErrorMessage;
+import com.vaadin.server.UserError;
import com.vaadin.tests.data.bean.Person;
import com.vaadin.ui.AbstractField;
@@ -189,6 +197,66 @@ public class BinderTest {
Assert.assertEquals("", nameField.getValue());
}
+ @Test
+ public void validate_notBound_noErrors() {
+ Binder<Person> binder = new Binder<>();
+
+ List<ValidationError<?>> errors = binder.validate();
+
+ Assert.assertTrue(errors.isEmpty());
+ }
+
+ @Test
+ public void bound_validatorsAreOK_noErrors() {
+ Binder<Person> binder = new Binder<>();
+ Binding<Person, String> binding = binder.forField(nameField);
+ binding.withValidator(Validator.alwaysPass()).bind(Person::getFirstName,
+ Person::setFirstName);
+
+ nameField.setComponentError(new UserError(""));
+ List<ValidationError<?>> errors = binder.validate();
+
+ Assert.assertTrue(errors.isEmpty());
+ Assert.assertNull(nameField.getComponentError());
+ }
+
+ @SuppressWarnings("serial")
+ @Test
+ public void bound_validatorsFail_errors() {
+ Binder<Person> binder = new Binder<>();
+ Binding<Person, String> binding = binder.forField(nameField);
+ binding.withValidator(Validator.alwaysPass());
+ String msg1 = "foo";
+ String msg2 = "bar";
+ binding.withValidator(new Validator<String>() {
+ @Override
+ public Result<String> apply(String value) {
+ return new SimpleResult<>(null, msg1);
+ }
+ });
+ binding.withValidator(value -> false, msg2);
+ binding.bind(Person::getFirstName, Person::setFirstName);
+
+ List<ValidationError<?>> errors = binder.validate();
+
+ Assert.assertEquals(2, errors.size());
+
+ Set<String> errorMessages = errors.stream()
+ .map(ValidationError::getMessage).collect(Collectors.toSet());
+ Assert.assertTrue(errorMessages.contains(msg1));
+ Assert.assertTrue(errorMessages.contains(msg2));
+
+ Set<?> fields = errors.stream().map(ValidationError::getField)
+ .collect(Collectors.toSet());
+ Assert.assertEquals(1, fields.size());
+ Assert.assertTrue(fields.contains(nameField));
+
+ ErrorMessage componentError = nameField.getComponentError();
+ Assert.assertNotNull(componentError);
+ Assert.assertEquals("foo",
+ ((AbstractErrorMessage) componentError).getMessage());
+ }
+
private void bindName() {
binder.bind(nameField, Person::getFirstName, Person::setFirstName);
binder.bind(p);
diff --git a/server/src/test/java/com/vaadin/data/ResultTest.java b/server/src/test/java/com/vaadin/data/ResultTest.java
new file mode 100644
index 0000000000..c01daa0123
--- /dev/null
+++ b/server/src/test/java/com/vaadin/data/ResultTest.java
@@ -0,0 +1,105 @@
+/*
+ * Copyright 2000-2014 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.function.Function;
+
+import org.junit.Assert;
+import org.junit.Test;
+
+/**
+ * @author Vaadin Ltd
+ *
+ */
+public class ResultTest {
+
+ @Test
+ public void testOk() {
+ String value = "foo";
+ Result<String> ok = Result.ok(value);
+ Assert.assertFalse(ok.isError());
+ Assert.assertFalse(ok.getMessage().isPresent());
+ ok.ifOk(v -> Assert.assertEquals(value, v));
+ ok.ifError(msg -> Assert.fail());
+ }
+
+ @Test
+ public void testError() {
+ String message = "foo";
+ Result<String> error = Result.error(message);
+ Assert.assertTrue(error.isError());
+ Assert.assertTrue(error.getMessage().isPresent());
+ error.ifOk(v -> Assert.fail());
+ error.ifError(msg -> Assert.assertEquals(message, msg));
+ Assert.assertEquals(message, error.getMessage().get());
+ }
+
+ @Test
+ public void of_noException() {
+ Result<String> result = Result.of(() -> "", exception -> null);
+ Assert.assertTrue(result instanceof SimpleResult);
+ Assert.assertFalse(((SimpleResult<?>) result).isError());
+ }
+
+ @Test
+ public void of_exception() {
+ String message = "foo";
+ Result<String> result = Result.<String> of(() -> {
+ throw new RuntimeException();
+ }, exception -> message);
+ Assert.assertTrue(result instanceof SimpleResult);
+ Assert.assertTrue(((SimpleResult<?>) result).isError());
+ Assert.assertEquals(message, result.getMessage().get());
+ }
+
+ @SuppressWarnings("serial")
+ @Test
+ public void map_norError_mapperIsApplied() {
+ Result<String> result = new SimpleResult<String>("foo", null) {
+
+ @Override
+ public <S> Result<S> flatMap(Function<String, Result<S>> mapper) {
+ return mapper.apply("foo");
+ }
+ };
+ Result<String> mapResult = result.map(value -> {
+ Assert.assertEquals("foo", value);
+ return "bar";
+ });
+ Assert.assertTrue(mapResult instanceof SimpleResult);
+ Assert.assertFalse(((SimpleResult<?>) mapResult).isError());
+ mapResult.ifOk(v -> Assert.assertEquals("bar", v));
+ }
+
+ @SuppressWarnings("serial")
+ @Test
+ public void map_error_mapperIsApplied() {
+ Result<String> result = new SimpleResult<String>("foo", null) {
+
+ @Override
+ public <S> Result<S> flatMap(Function<String, Result<S>> mapper) {
+ return new SimpleResult<S>(null, "bar");
+ }
+ };
+ Result<String> mapResult = result.map(value -> {
+ Assert.assertEquals("foo", value);
+ return "somevalue";
+ });
+ Assert.assertTrue(mapResult instanceof SimpleResult);
+ Assert.assertTrue(((SimpleResult<?>) mapResult).isError());
+ mapResult.ifError(msg -> Assert.assertEquals("bar", msg));
+ }
+}
diff --git a/server/src/test/java/com/vaadin/data/ValidatorTest.java b/server/src/test/java/com/vaadin/data/ValidatorTest.java
new file mode 100644
index 0000000000..1a432c063c
--- /dev/null
+++ b/server/src/test/java/com/vaadin/data/ValidatorTest.java
@@ -0,0 +1,93 @@
+/*
+ * Copyright 2000-2014 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.Objects;
+
+import org.junit.Assert;
+import org.junit.Test;
+
+/**
+ * @author Vaadin Ltd
+ *
+ */
+public class ValidatorTest {
+
+ @Test
+ public void alwaysPass() {
+ Validator<String> alwaysPass = Validator.alwaysPass();
+ Result<String> result = alwaysPass.apply("foo");
+ Assert.assertTrue(result instanceof SimpleResult);
+ SimpleResult<String> implRes = (SimpleResult<String>) result;
+ Assert.assertFalse(implRes.getMessage().isPresent());
+ }
+
+ @Test
+ public void chain_alwaysPassAndError() {
+ Validator<String> alwaysPass = Validator.alwaysPass();
+ Validator<String> chain = alwaysPass
+ .chain(value -> Result.error("foo"));
+ Result<String> result = chain.apply("bar");
+ Assert.assertTrue(result.isError());
+ Assert.assertEquals("foo", result.getMessage().get());
+ }
+
+ @SuppressWarnings("serial")
+ @Test
+ public void chain_mixture() {
+ Validator<String> first = new Validator<String>() {
+
+ @Override
+ public Result<String> apply(String value) {
+ if (value == null) {
+ return Result.error("Cannot be null");
+ }
+ return Result.ok(value);
+ }
+ };
+ Validator<String> second = new Validator<String>() {
+
+ @Override
+ public Result<String> apply(String value) {
+ if (value != null && value.isEmpty()) {
+ return Result.error("Cannot be empty");
+ }
+ return Result.ok(value);
+ }
+ };
+
+ Validator<String> chain = first.chain(second);
+ Result<String> result = chain.apply("bar");
+ Assert.assertFalse(result.isError());
+
+ result = chain.apply(null);
+ Assert.assertTrue(result.isError());
+
+ result = chain.apply("");
+ Assert.assertTrue(result.isError());
+ }
+
+ @Test
+ public void from() {
+ Validator<String> validator = Validator.from(Objects::nonNull,
+ "Cannot be null");
+ Result<String> result = validator.apply(null);
+ Assert.assertTrue(result.isError());
+
+ result = validator.apply("");
+ Assert.assertFalse(result.isError());
+ }
+}