aboutsummaryrefslogtreecommitdiffstats
path: root/server/src/main
diff options
context:
space:
mode:
authorDenis Anisimov <denis@vaadin.com>2016-08-08 15:46:52 +0300
committerDenis Anisimov <denis@vaadin.com>2016-08-10 09:44:34 +0300
commit8139cd8c89ee28aafc3bb0a1ea39e9a2697fef77 (patch)
tree0d76a3e3bce1df853c2896c2bc81546a9401d4cb /server/src/main
parentfd651d7921dcfb5ae7e89dee0d538e1072eeb00d (diff)
downloadvaadin-framework-8139cd8c89ee28aafc3bb0a1ea39e9a2697fef77.tar.gz
vaadin-framework-8139cd8c89ee28aafc3bb0a1ea39e9a2697fef77.zip
Binding.withValidator and Binder.validate methods (#26).
Change-Id: I0641ea6118cd873c803d3c21d82b14fe8db4baa2
Diffstat (limited to 'server/src/main')
-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
5 files changed, 600 insertions, 3 deletions
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);
+ }
+ };
+ }
+}