diff options
author | Pekka Hyvönen <pekka@vaadin.com> | 2016-08-26 00:29:50 +0300 |
---|---|---|
committer | Vaadin Code Review <review@vaadin.com> | 2016-09-08 12:15:24 +0000 |
commit | 222908a9372885cc05bc3cb04374aea5aba66139 (patch) | |
tree | 7b63189c5256176126947000a221bb5aeccbee42 /server/src/main | |
parent | 3017820a537808c3b6baa337a17f2a8f1585d543 (diff) | |
download | vaadin-framework-222908a9372885cc05bc3cb04374aea5aba66139.tar.gz vaadin-framework-222908a9372885cc05bc3cb04374aea5aba66139.zip |
Add Form level status handler and status label
This feature doesn't make a whole lot of sense until
form level status changes are available.
Change-Id: Ie634c4a6b3511b7cbf9e367192034934b0e0d4b0
Diffstat (limited to 'server/src/main')
5 files changed, 295 insertions, 25 deletions
diff --git a/server/src/main/java/com/vaadin/data/Binder.java b/server/src/main/java/com/vaadin/data/Binder.java index cf7f788677..9150c046f6 100644 --- a/server/src/main/java/com/vaadin/data/Binder.java +++ b/server/src/main/java/com/vaadin/data/Binder.java @@ -17,6 +17,7 @@ package com.vaadin.data; import java.io.Serializable; import java.util.ArrayList; +import java.util.Arrays; import java.util.Collections; import java.util.HashMap; import java.util.LinkedHashSet; @@ -505,22 +506,25 @@ public class Binder<BEAN> implements Serializable { @Override public Result<TARGET> validate() { - Result<TARGET> dataValue = getTargetValue(); - fireStatusChangeEvent(dataValue); - return dataValue; + BinderResult<FIELDVALUE, TARGET> bindingResult = getTargetValue(); + getBinder().getStatusHandler().accept(Arrays.asList(bindingResult)); + return bindingResult; } /** - * Returns the field value run through all converters and validators. + * Returns the field value run through all converters and validators, + * but doesn't fire a {@link ValidationStatusChangeEvent status change + * event}. * * @return a result containing the validated and converted value or * describing an error */ - private Result<TARGET> getTargetValue() { + private BinderResult<FIELDVALUE, TARGET> getTargetValue() { FIELDVALUE fieldValue = field.getValue(); Result<TARGET> dataValue = converterValidatorChain.convertToModel( fieldValue, ((AbstractComponent) field).getLocale()); - return dataValue; + return dataValue.biMap((value, message) -> new BinderResult<>(this, + value, message)); } private void unbind() { @@ -561,14 +565,15 @@ public class Binder<BEAN> implements Serializable { boolean runBeanLevelValidation) { assert bean != null; if (setter != null) { - getTargetValue().ifOk(value -> setBeanValue(bean, value)); + 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)) { - List<ValidationError<?>> errors = binder.validateItem(bean); - // TODO: Pass errors to Binder statusChangeHandler once that is - // available + binder.validateItem(bean); } } @@ -576,7 +581,7 @@ public class Binder<BEAN> implements Serializable { setter.accept(bean, value); } - private void fireStatusChangeEvent(Result<TARGET> result) { + private void fireStatusChangeEvent(Result<?> result) { ValidationStatusChangeEvent event = new ValidationStatusChangeEvent( getField(), result.isError() ? ValidationStatus.ERROR @@ -632,6 +637,10 @@ public class Binder<BEAN> implements Serializable { private final List<Validator<? super BEAN>> validators = new ArrayList<>(); + private Label statusLabel; + + private BinderStatusHandler statusHandler; + /** * Returns an {@code Optional} of the bean that has been bound with * {@link #bind}, or an empty optional if a bean is not currently bound. @@ -909,6 +918,9 @@ public class Binder<BEAN> implements Serializable { * If all validators pass, the resulting list is empty. * <p> * Does not run bean validators. + * <p> + * All results are passed to the {@link #getStatusHandler() status change + * handler.} * * @see #validateItem(Object) * @@ -916,13 +928,17 @@ public class Binder<BEAN> implements Serializable { * succeeded */ private List<ValidationError<?>> validateBindings() { - List<ValidationError<?>> resultErrors = new ArrayList<>(); + List<BinderResult<?, ?>> results = new ArrayList<>(); for (BindingImpl<?, ?, ?> binding : bindings) { - binding.validate().ifError(errorMessage -> resultErrors - .add(new ValidationError<>(binding, - binding.getField().getValue(), errorMessage))); + results.add(binding.getTargetValue()); } - return resultErrors; + + 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()); } /** @@ -941,13 +957,100 @@ public class Binder<BEAN> implements Serializable { */ private List<ValidationError<?>> validateItem(BEAN bean) { Objects.requireNonNull(bean, "bean cannot be null"); - return validators.stream().map(validator -> validator.apply(bean)) + List<BinderResult<?, ?>> 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()); } /** + * Sets the label to show the binder level validation errors not related to + * any specific field. + * <p> + * Only the one validation error message is shown in this label at a time. + * <p> + * This is a convenience method for + * {@link #setStatusHandler(BinderStatusHandler)}, which means that this + * method cannot be used after the handler has been set. Also the handler + * cannot be set after this label has been set. + * + * @param statusLabel + * the status label to set + * @see #setStatusHandler(BinderStatusHandler) + * @see Binding#withStatusLabel(Label) + */ + public void setStatusLabel(Label statusLabel) { + if (statusHandler != null) { + throw new IllegalStateException("Cannot set status label if a " + + BinderStatusHandler.class.getSimpleName() + + " has already been set."); + } + this.statusLabel = statusLabel; + } + + /** + * Gets the status label or an empty optional if none has been set. + * + * @return the optional status label + * @see #setStatusLabel(Label) + */ + public Optional<Label> getStatusLabel() { + return Optional.ofNullable(statusLabel); + } + + /** + * Sets the status handler to track form status changes. + * <p> + * Setting this handler will override the default behavior, which is to let + * fields show their validation status messages and show binder level + * validation errors or OK status in the label set with + * {@link #setStatusLabel(Label)}. + * <p> + * This handler cannot be set after the status label has been set with + * {@link #setStatusLabel(Label)}, or {@link #setStatusLabel(Label)} cannot + * be used after this handler has been set. + * + * @param statusHandler + * the status handler to set, not <code>null</code> + * @throws NullPointerException + * for <code>null</code> status handler + * @see #setStatusLabel(Label) + * @see Binding#withStatusChangeHandler(StatusChangeHandler) + */ + public void setStatusHandler(BinderStatusHandler statusHandler) { + Objects.requireNonNull(statusHandler, "Cannot set a null " + + BinderStatusHandler.class.getSimpleName()); + if (statusLabel != null) { + throw new IllegalStateException( + "Cannot set " + BinderStatusHandler.class.getSimpleName() + + " if a status label has already been set."); + } + this.statusHandler = statusHandler; + } + + /** + * Gets the status handler of this form. + * <p> + * If none has been set with {@link #setStatusHandler(BinderStatusHandler)}, + * the default implementation is returned. + * + * @return the status handler used, never <code>null</code> + * @see #setStatusHandler(BinderStatusHandler) + */ + public BinderStatusHandler getStatusHandler() { + return Optional.ofNullable(statusHandler) + .orElse(this::defaultHandleBinderStatusChange); + } + + /** * Creates a new binding with the given field. * * @param <FIELDVALUE> @@ -1017,4 +1120,33 @@ public class Binder<BEAN> implements Serializable { } } + /** + * The default binder level status handler. + * <p> + * Passes all field related results to the Binding status handlers. All + * other status changes are displayed in the status label, if one has been + * set with {@link #setStatusLabel(Label)}. + * + * @param results + * a list of validation results from binding and/or item level + * validators + */ + @SuppressWarnings("unchecked") + protected void defaultHandleBinderStatusChange( + List<BinderResult<?, ?>> results) { + // let field events go to binding status handlers + results.stream().filter(br -> br.getField().isPresent()) + .forEach(br -> ((BindingImpl<BEAN, ?, ?>) br.getBinding().get()) + .fireStatusChangeEvent(br)); + + // 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() + .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 new file mode 100644 index 0000000000..52375b88ff --- /dev/null +++ b/server/src/main/java/com/vaadin/data/BinderResult.java @@ -0,0 +1,72 @@ +/* + * Copyright 2000-2016 Vaadin Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ +package com.vaadin.data; + +import java.util.Optional; + +import com.vaadin.data.Binder.Binding; + +/** + * A result that keeps track of the possible binding (field) it belongs to. + * + * @param <FIELDVALUE> + * the value type of the field + * @param <VALUE> + * the result value type and the data type of the binding, matches + * the field type if a converter has not been set + */ +public class BinderResult<FIELDVALUE, VALUE> extends SimpleResult<VALUE> { + + private final Binding<?, FIELDVALUE, VALUE> binding; + + /** + * Creates a new binder result. + * + * @param binding + * the binding where the result originated, may be {@code null} + * @param value + * the resut value, can be <code>null</code> + * @param message + * the error message of the result, may be {@code null} + */ + public BinderResult(Binding<?, FIELDVALUE, VALUE> binding, VALUE value, + String message) { + super(value, message); + this.binding = binding; + } + + /** + * Return the binding this result originated from, or an empty optional if + * none. + * + * @return the optional binding + */ + public Optional<Binding<?, FIELDVALUE, VALUE>> getBinding() { + return Optional.ofNullable(binding); + } + + /** + * Return the field this result originated from, or an empty optional if + * none. + * + * @return the optional field + */ + public Optional<HasValue<FIELDVALUE>> getField() { + return binding == null ? Optional.empty() + : Optional.ofNullable(binding.getField()); + } + +}
\ No newline at end of file diff --git a/server/src/main/java/com/vaadin/data/BinderStatusHandler.java b/server/src/main/java/com/vaadin/data/BinderStatusHandler.java new file mode 100644 index 0000000000..4c516a5ed6 --- /dev/null +++ b/server/src/main/java/com/vaadin/data/BinderStatusHandler.java @@ -0,0 +1,46 @@ +/* + * 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.List; +import java.util.function.Consumer; + +import com.vaadin.data.Binder.Binding; + +/** + * Status change handler for forms. + * <p> + * Register a handler using {@link Binder#setStatusHandler(BinderStatusHandler)} + * to be able to customize the status change handling such as displaying + * validation messages. + * <p> + * The list will contain results for either binding level or binder level, but + * never both mixed. This is because binder level validation is not run if + * binding level validation fails. + * + * @see Binder#setStatusHandler(BinderStatusHandler) + * @see Binder#setStatusLabel(com.vaadin.ui.Label) + * @see Binding#withStatusChangeHandler(StatusChangeHandler) + * + * @author Vaadin Ltd + * @since 8.0 + * + */ +public interface BinderStatusHandler + extends Consumer<List<BinderResult<?, ?>>>, Serializable { + +} diff --git a/server/src/main/java/com/vaadin/data/Result.java b/server/src/main/java/com/vaadin/data/Result.java index b82b3ff4e7..5803eace11 100644 --- a/server/src/main/java/com/vaadin/data/Result.java +++ b/server/src/main/java/com/vaadin/data/Result.java @@ -19,6 +19,7 @@ package com.vaadin.data; import java.io.Serializable; import java.util.Objects; import java.util.Optional; +import java.util.function.BiFunction; import java.util.function.Consumer; import java.util.function.Function; import java.util.function.Supplier; @@ -103,7 +104,7 @@ public interface Result<R> extends Serializable { * the mapping function * @return the mapped result */ - public default <S> Result<S> map(Function<R, S> mapper) { + default <S> Result<S> map(Function<R, S> mapper) { return flatMap(value -> ok(mapper.apply(value))); } @@ -119,7 +120,20 @@ public interface Result<R> extends Serializable { * the mapping function * @return the mapped result */ - public <S> Result<S> flatMap(Function<R, Result<S>> mapper); + <S> Result<S> flatMap(Function<R, Result<S>> mapper); + + /** + * Applies the given function to this result, regardless if this is an error + * or not. Passes the value and the message to the given function as + * parameters. + * + * @param <S> + * the type of the mapped value + * @param mapper + * the mapping function + * @return the mapped result + */ + <S> S biMap(BiFunction<R, String, S> mapper); /** * Invokes either the first callback or the second one, depending on whether @@ -130,7 +144,7 @@ public interface Result<R> extends Serializable { * @param ifError * the function to call if failure */ - public void handle(Consumer<R> ifOk, Consumer<String> ifError); + void handle(Consumer<R> ifOk, Consumer<String> ifError); /** * Applies the {@code consumer} if result is not an error. @@ -138,7 +152,7 @@ public interface Result<R> extends Serializable { * @param consumer * consumer to apply in case it's not an error */ - public default void ifOk(Consumer<R> consumer) { + default void ifOk(Consumer<R> consumer) { handle(consumer, error -> { }); } @@ -149,7 +163,7 @@ public interface Result<R> extends Serializable { * @param consumer * consumer to apply in case it's an error */ - public default void ifError(Consumer<String> consumer) { + default void ifError(Consumer<String> consumer) { handle(value -> { }, consumer); } @@ -160,14 +174,14 @@ public interface Result<R> extends Serializable { * @return <code>true</code> if the result denotes an error, * <code>false</code> otherwise */ - public boolean isError(); + boolean isError(); /** * Returns an Optional of the result message, or an empty Optional if none. * * @return the optional message */ - public Optional<String> getMessage(); + Optional<String> getMessage(); /** * Return the value, if the result denotes success, otherwise throw an @@ -182,6 +196,6 @@ public interface Result<R> extends Serializable { * @throws X * if this result denotes an error */ - public <X extends Throwable> R getOrThrow( + <X extends Throwable> R getOrThrow( Function<String, ? extends X> exceptionProvider) throws X; } diff --git a/server/src/main/java/com/vaadin/data/SimpleResult.java b/server/src/main/java/com/vaadin/data/SimpleResult.java index 935fb545e3..ceedcb5ae3 100644 --- a/server/src/main/java/com/vaadin/data/SimpleResult.java +++ b/server/src/main/java/com/vaadin/data/SimpleResult.java @@ -17,6 +17,7 @@ package com.vaadin.data; import java.util.Objects; import java.util.Optional; +import java.util.function.BiFunction; import java.util.function.Consumer; import java.util.function.Function; @@ -65,6 +66,11 @@ class SimpleResult<R> implements Result<R> { } @Override + public <S> S biMap(BiFunction<R, String, S> mapper) { + return mapper.apply(value, message); + } + + @Override public void handle(Consumer<R> ifOk, Consumer<String> ifError) { Objects.requireNonNull(ifOk, "ifOk cannot be null"); Objects.requireNonNull(ifError, "ifError cannot be null"); |