aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorPekka Hyvönen <pekka@vaadin.com>2016-08-26 00:29:50 +0300
committerVaadin Code Review <review@vaadin.com>2016-09-08 12:15:24 +0000
commit222908a9372885cc05bc3cb04374aea5aba66139 (patch)
tree7b63189c5256176126947000a221bb5aeccbee42
parent3017820a537808c3b6baa337a17f2a8f1585d543 (diff)
downloadvaadin-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
-rw-r--r--documentation/datamodel/datamodel-forms.asciidoc6
-rw-r--r--server/src/main/java/com/vaadin/data/Binder.java166
-rw-r--r--server/src/main/java/com/vaadin/data/BinderResult.java72
-rw-r--r--server/src/main/java/com/vaadin/data/BinderStatusHandler.java46
-rw-r--r--server/src/main/java/com/vaadin/data/Result.java30
-rw-r--r--server/src/main/java/com/vaadin/data/SimpleResult.java6
-rw-r--r--server/src/test/java/com/vaadin/data/BinderBookOfVaadinTest.java128
-rw-r--r--server/src/test/java/com/vaadin/data/BinderTest.java212
8 files changed, 628 insertions, 38 deletions
diff --git a/documentation/datamodel/datamodel-forms.asciidoc b/documentation/datamodel/datamodel-forms.asciidoc
index b86cf8f672..62fd9df38c 100644
--- a/documentation/datamodel/datamodel-forms.asciidoc
+++ b/documentation/datamodel/datamodel-forms.asciidoc
@@ -566,21 +566,21 @@ We can also define our own status handler to provide a custom way of handling st
----
BinderStatusHandler defaultHandler = binder.getStatusHandler();
-binder.setStatusHandler((List<BinderResult> results) -> {
+binder.setStatusHandler(results -> {
String errorMessage = results.stream()
// Ignore helper and confirmation messages
.filter(BinderResult::isError)
// Ignore messages that belong to a specific field
.filter(error -> !error.getField().isPresent())
// Create a string out of the remaining messages
- .map(BinderResult::getMessage)
+ .map(Result::getMessage).map(o -> o.get())
.collect(Collectors.joining("\n"));
formStatusLabel.setValue(errorMessage);
formStatusLabel.setVisible(!errorMessage.isEmpty());
// Let the default handler show messages for each field
- defaultHandler.handleStatus(results);
+ defaultHandler.accept(event);
});
----
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");
diff --git a/server/src/test/java/com/vaadin/data/BinderBookOfVaadinTest.java b/server/src/test/java/com/vaadin/data/BinderBookOfVaadinTest.java
index c51d41d6a7..6861903132 100644
--- a/server/src/test/java/com/vaadin/data/BinderBookOfVaadinTest.java
+++ b/server/src/test/java/com/vaadin/data/BinderBookOfVaadinTest.java
@@ -20,6 +20,7 @@ import java.util.Date;
import java.util.List;
import java.util.Locale;
import java.util.concurrent.atomic.AtomicReference;
+import java.util.stream.Collectors;
import org.junit.Assert;
import org.junit.Before;
@@ -399,7 +400,7 @@ public class BinderBookOfVaadinTest {
}
@Test
- public void withStatusChangeHandlerExample() {
+ public void withBindingStatusChangeHandlerExample() {
Label nameStatus = new Label();
AtomicReference<ValidationStatusChangeEvent> event = new AtomicReference<>();
@@ -555,4 +556,129 @@ public class BinderBookOfVaadinTest {
binder.load(p);
Assert.assertEquals("12500", yearOfBirthField.getValue());
}
+
+ @Test
+ public void withBinderStatusLabelExample() {
+ Label formStatusLabel = new Label();
+
+ BeanBinder<BookPerson> binder = new BeanBinder<>(BookPerson.class);
+
+ binder.setStatusLabel(formStatusLabel);
+
+ final String message = "Too young, son";
+ final String message2 = "Y2K error";
+ TextField yearOfBirth = new TextField();
+ BookPerson p = new BookPerson(1500, 12);
+ binder.forField(yearOfBirth)
+ .withConverter(new StringToIntegerConverter("err"))
+ .bind(BookPerson::getYearOfBirth, BookPerson::setYearOfBirth);
+ binder.withValidator(bean -> bean.yearOfBirth < 2000 ? Result.ok(bean)
+ : Result.error(message))
+ .withValidator(bean -> bean.yearOfBirth == 2000
+ ? Result.error(message2) : Result.ok(bean));
+
+ binder.bind(p);
+
+ // first bean validator fails and passes error message to status label
+ yearOfBirth.setValue("2001");
+
+ List<ValidationError<?>> errors = binder.validate();
+ Assert.assertEquals(1, errors.size());
+ Assert.assertEquals(errors.get(0).getMessage(), message);
+
+ Assert.assertEquals(message, formStatusLabel.getValue());
+
+ // value is correct, status label is cleared
+ yearOfBirth.setValue("1999");
+
+ errors = binder.validate();
+ Assert.assertEquals(0, errors.size());
+
+ Assert.assertEquals("", formStatusLabel.getValue());
+
+ // both bean validators fail, should be two error messages chained
+ yearOfBirth.setValue("2000");
+
+ errors = binder.validate();
+ Assert.assertEquals(2, errors.size());
+
+ // only first error is shown
+ Assert.assertEquals(message, formStatusLabel.getValue());
+ }
+
+ @Test
+ public void withBinderStatusChangeHandlerExample() {
+ Label formStatusLabel = new Label();
+
+ BinderStatusHandler defaultHandler = binder.getStatusHandler();
+
+ binder.setStatusHandler(results -> {
+ String errorMessage = results.stream()
+ // Ignore confirmation messages
+ .filter(BinderResult::isError)
+ // Ignore messages that belong to a specific field
+ .filter(error -> !error.getField().isPresent())
+ // Create a string out of the remaining messages
+ .map(Result::getMessage).map(o -> o.get())
+ .collect(Collectors.joining("\n"));
+
+ formStatusLabel.setValue(errorMessage);
+ formStatusLabel.setVisible(!errorMessage.isEmpty());
+
+ // Let the default handler show messages for each field
+ defaultHandler.accept(results);
+ });
+
+ final String bindingMessage = "uneven";
+ final String message = "Too young, son";
+ final String message2 = "Y2K error";
+ TextField yearOfBirth = new TextField();
+ BookPerson p = new BookPerson(1500, 12);
+ binder.forField(yearOfBirth)
+ .withConverter(new StringToIntegerConverter("err"))
+ .withValidator(value -> value % 2 == 0 ? Result.ok(value)
+ : Result.error(bindingMessage))
+ .bind(BookPerson::getYearOfBirth, BookPerson::setYearOfBirth);
+ binder.withValidator(bean -> bean.yearOfBirth < 2000 ? Result.ok(bean)
+ : Result.error(message))
+ .withValidator(bean -> bean.yearOfBirth == 2000
+ ? Result.error(message2) : Result.ok(bean));
+
+ binder.bind(p);
+
+ // first binding validation fails, no bean level validation is done
+ yearOfBirth.setValue("2001");
+ List<ValidationError<?>> errors = binder.validate();
+ Assert.assertEquals(1, errors.size());
+ Assert.assertEquals(errors.get(0).getMessage(), bindingMessage);
+
+ Assert.assertEquals("", formStatusLabel.getValue());
+
+ // first bean validator fails and passes error message to status label
+ yearOfBirth.setValue("2002");
+
+ errors = binder.validate();
+ Assert.assertEquals(1, errors.size());
+ Assert.assertEquals(errors.get(0).getMessage(), message);
+
+ Assert.assertEquals(message, formStatusLabel.getValue());
+
+ // value is correct, status label is cleared
+ yearOfBirth.setValue("1998");
+
+ errors = binder.validate();
+ Assert.assertEquals(0, errors.size());
+
+ Assert.assertEquals("", formStatusLabel.getValue());
+
+ // both bean validators fail, should be two error messages chained
+ yearOfBirth.setValue("2000");
+
+ errors = binder.validate();
+ Assert.assertEquals(2, errors.size());
+
+ Assert.assertEquals(message + "\n" + message2,
+ formStatusLabel.getValue());
+
+ }
}
diff --git a/server/src/test/java/com/vaadin/data/BinderTest.java b/server/src/test/java/com/vaadin/data/BinderTest.java
index 4c59773b2f..e6b78eb6b3 100644
--- a/server/src/test/java/com/vaadin/data/BinderTest.java
+++ b/server/src/test/java/com/vaadin/data/BinderTest.java
@@ -577,7 +577,7 @@ public class BinderTest {
Assert.assertNull(event.get());
event.set(evt);
});
- binding.bind(Person::getFirstName, Person::setLastName);
+ binding.bind(Person::getFirstName, Person::setFirstName);
nameField.setValue("");
@@ -610,7 +610,7 @@ public class BinderTest {
Binding<Person, String, String> binding = binder.forField(nameField)
.withValidator(notEmpty).withStatusChangeHandler(evt -> {
});
- binding.bind(Person::getFirstName, Person::setLastName);
+ binding.bind(Person::getFirstName, Person::setFirstName);
Assert.assertNull(nameField.getComponentError());
@@ -630,7 +630,7 @@ public class BinderTest {
Binding<Person, String, String> binding = binder.forField(nameField)
.withValidator(notEmpty).withStatusLabel(label);
- binding.bind(Person::getFirstName, Person::setLastName);
+ binding.bind(Person::getFirstName, Person::setFirstName);
nameField.setValue("");
@@ -657,7 +657,7 @@ public class BinderTest {
Binding<Person, String, String> binding = binder.forField(nameField)
.withValidator(notEmpty).withStatusLabel(label);
- binding.bind(Person::getFirstName, Person::setLastName);
+ binding.bind(Person::getFirstName, Person::setFirstName);
Assert.assertNull(nameField.getComponentError());
@@ -675,7 +675,7 @@ public class BinderTest {
public void bindingWithStatusChangeHandler_addAfterBound() {
Binding<Person, String, String> binding = binder.forField(nameField)
.withValidator(notEmpty);
- binding.bind(Person::getFirstName, Person::setLastName);
+ binding.bind(Person::getFirstName, Person::setFirstName);
binding.withStatusChangeHandler(evt -> Assert.fail());
}
@@ -686,7 +686,7 @@ public class BinderTest {
Binding<Person, String, String> binding = binder.forField(nameField)
.withValidator(notEmpty);
- binding.bind(Person::getFirstName, Person::setLastName);
+ binding.bind(Person::getFirstName, Person::setFirstName);
binding.withStatusLabel(label);
}
@@ -696,7 +696,6 @@ public class BinderTest {
Label label = new Label();
Binding<Person, String, String> binding = binder.forField(nameField);
- binding.bind(Person::getFirstName, Person::setLastName);
binding.withStatusChangeHandler(event -> {
});
@@ -709,7 +708,6 @@ public class BinderTest {
Label label = new Label();
Binding<Person, String, String> binding = binder.forField(nameField);
- binding.bind(Person::getFirstName, Person::setLastName);
binding.withStatusLabel(label);
@@ -721,7 +719,6 @@ public class BinderTest {
public void bingingWithStatusChangeHandler_setAfterOtherHandler() {
Binding<Person, String, String> binding = binder.forField(nameField);
- binding.bind(Person::getFirstName, Person::setLastName);
binding.withStatusChangeHandler(event -> {
});
@@ -865,4 +862,201 @@ public class BinderTest {
Assert.assertTrue(beanLevelValidationRun.get());
}
+ @Test
+ public void binderWithStatusChangeHandler_handlerGetsEvents() {
+ AtomicReference<List<BinderResult<?, ?>>> resultsCapture = new AtomicReference<>();
+ binder.forField(nameField).withValidator(notEmpty)
+ .withStatusChangeHandler(evt -> {
+ Assert.fail(
+ "Using a custom status change handler so no change should end up here");
+ }).bind(Person::getFirstName, Person::setFirstName);
+ binder.forField(ageField).withConverter(stringToInteger)
+ .withValidator(notNegative).withStatusChangeHandler(evt -> {
+ Assert.fail(
+ "Using a custom status change handler so no change should end up here");
+ }).bind(Person::getAge, Person::setAge);
+ binder.withValidator(
+ bean -> !bean.getFirstName().isEmpty() && bean.getAge() > 0
+ ? Result.ok(bean)
+ : Result.error("Need first name and age"));
+
+ binder.setStatusHandler(r -> {
+ resultsCapture.set(r);
+ });
+ binder.bind(p);
+ Assert.assertNull(nameField.getComponentError());
+
+ nameField.setValue("");
+ ageField.setValue("5");
+
+ // First binding validation fails => should be result with ERROR status
+ // and message
+ binder.validate();
+
+ Assert.assertNull(nameField.getComponentError());
+
+ List<BinderResult<?, ?>> results = resultsCapture.get();
+ Assert.assertNotNull(results);
+ Assert.assertEquals(2, results.size());
+
+ BinderResult<?, ?> r = results.get(0);
+ Assert.assertTrue(r.isError());
+ Assert.assertEquals("Value cannot be empty", r.getMessage().get());
+ Assert.assertEquals(nameField, r.getField().get());
+
+ r = results.get(1);
+ Assert.assertFalse(r.isError());
+ Assert.assertFalse(r.getMessage().isPresent());
+ Assert.assertEquals(ageField, r.getField().get());
+
+ nameField.setValue("foo");
+ ageField.setValue("");
+
+ resultsCapture.set(null);
+ // Second validation succeeds => should be result with OK status and
+ // no message, and error result for age
+ binder.validate();
+
+ results = resultsCapture.get();
+ Assert.assertNotNull(results);
+ Assert.assertEquals(2, results.size());
+
+ r = results.get(0);
+ Assert.assertFalse(r.isError());
+ Assert.assertFalse(r.getMessage().isPresent());
+ Assert.assertEquals(nameField, r.getField().get());
+
+ r = results.get(1);
+ Assert.assertTrue(r.isError());
+ Assert.assertEquals("Value must be a number", r.getMessage().get());
+ Assert.assertEquals(ageField, r.getField().get());
+
+ resultsCapture.set(null);
+ // binding validations pass, binder validation fails
+ ageField.setValue("0");
+ binder.validate();
+
+ results = resultsCapture.get();
+ Assert.assertNotNull(results);
+ Assert.assertEquals(1, results.size());
+
+ r = results.get(0);
+ Assert.assertTrue(r.isError());
+ Assert.assertTrue(r.getMessage().isPresent());
+ Assert.assertFalse(r.getField().isPresent());
+ }
+
+ @Test
+ public void binderWithStatusChangeHandler_defaultStatusChangeHandlerIsReplaced() {
+ Binding<Person, String, String> binding = binder.forField(nameField)
+ .withValidator(notEmpty).withStatusChangeHandler(evt -> {
+ });
+ binding.bind(Person::getFirstName, Person::setFirstName);
+
+ Assert.assertNull(nameField.getComponentError());
+
+ nameField.setValue("");
+
+ // First validation fails => should be event with ERROR status and
+ // message
+ binding.validate();
+
+ // no component error since default handler is replaced
+ Assert.assertNull(nameField.getComponentError());
+ }
+
+ @Test
+ public void binderWithStatusLabel_defaultStatusChangeHandlerIsReplaced() {
+ Label label = new Label();
+
+ Binding<Person, String, String> binding = binder.forField(nameField)
+ .withValidator(notEmpty).withStatusLabel(label);
+ binding.bind(Person::getFirstName, Person::setFirstName);
+
+ Assert.assertNull(nameField.getComponentError());
+
+ nameField.setValue("");
+
+ // First validation fails => should be event with ERROR status and
+ // message
+ binding.validate();
+
+ // default behavior should update component error for the nameField
+ Assert.assertNull(nameField.getComponentError());
+ }
+
+ @Test(expected = IllegalStateException.class)
+ public void binderWithStatusChangeHandler_addAfterBound() {
+ Binding<Person, String, String> binding = binder.forField(nameField)
+ .withValidator(notEmpty);
+ binding.bind(Person::getFirstName, Person::setFirstName);
+
+ binding.withStatusChangeHandler(evt -> Assert.fail());
+ }
+
+ @Test(expected = IllegalStateException.class)
+ public void binderWithStatusLabel_addAfterBound() {
+ Label label = new Label();
+
+ Binding<Person, String, String> binding = binder.forField(nameField)
+ .withValidator(notEmpty);
+ binding.bind(Person::getFirstName, Person::setFirstName);
+
+ binding.withStatusLabel(label);
+ }
+
+ @Test(expected = IllegalStateException.class)
+ public void binderWithStatusLabel_setAfterHandler() {
+ Label label = new Label();
+
+ Binding<Person, String, String> binding = binder.forField(nameField);
+ binding.bind(Person::getFirstName, Person::setFirstName);
+
+ binder.setStatusHandler(event -> {
+ });
+
+ binder.setStatusLabel(label);
+ }
+
+ @Test(expected = IllegalStateException.class)
+ public void binderWithStatusChangeHandler_setAfterLabel() {
+ Label label = new Label();
+
+ Binding<Person, String, String> binding = binder.forField(nameField);
+ binding.bind(Person::getFirstName, Person::setFirstName);
+
+ binder.setStatusLabel(label);
+
+ binder.setStatusHandler(event -> {
+ });
+ }
+
+ @Test(expected = NullPointerException.class)
+ public void binderWithNullStatusChangeHandler_throws() {
+ binder.setStatusHandler(null);
+ }
+
+ @Test
+ public void binderWithStatusChangeHandler_replaceHandler() {
+ AtomicReference<List<BinderResult<?, ?>>> capture = new AtomicReference<>();
+
+ Binding<Person, String, String> binding = binder.forField(nameField);
+ binding.bind(Person::getFirstName, Person::setFirstName);
+
+ binder.setStatusHandler(results -> {
+ Assert.fail();
+ });
+
+ binder.setStatusHandler(results -> {
+ capture.set(results);
+ });
+
+ nameField.setValue("foo");
+ binder.validate();
+
+ List<BinderResult<?, ?>> results = capture.get();
+ Assert.assertNotNull(results);
+ Assert.assertEquals(1, results.size());
+ }
+
}