diff options
author | Denis Anisimov <denis@vaadin.com> | 2016-08-08 15:46:52 +0300 |
---|---|---|
committer | Denis Anisimov <denis@vaadin.com> | 2016-08-10 09:44:34 +0300 |
commit | 8139cd8c89ee28aafc3bb0a1ea39e9a2697fef77 (patch) | |
tree | 0d76a3e3bce1df853c2896c2bc81546a9401d4cb /server/src/main | |
parent | fd651d7921dcfb5ae7e89dee0d538e1072eeb00d (diff) | |
download | vaadin-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.java | 133 | ||||
-rw-r--r-- | server/src/main/java/com/vaadin/data/Result.java | 170 | ||||
-rw-r--r-- | server/src/main/java/com/vaadin/data/SimpleResult.java | 96 | ||||
-rw-r--r-- | server/src/main/java/com/vaadin/data/ValidationError.java | 68 | ||||
-rw-r--r-- | server/src/main/java/com/vaadin/data/Validator.java | 136 |
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<Integer> 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<Integer> 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<Integer> 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); + } + }; + } +} |