----
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);
});
----
import java.io.Serializable;
import java.util.ArrayList;
+import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.LinkedHashSet;
@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() {
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);
}
}
setter.accept(bean, value);
}
- private void fireStatusChangeEvent(Result<TARGET> result) {
+ private void fireStatusChangeEvent(Result<?> result) {
ValidationStatusChangeEvent event = new ValidationStatusChangeEvent(
getField(),
result.isError() ? ValidationStatus.ERROR
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.
* 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)
*
* 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());
}
/**
*/
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.
*
}
}
+ /**
+ * 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);
+ }
+ }
+
}
--- /dev/null
+/*
+ * 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
--- /dev/null
+/*
+ * 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 {
+
+}
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;
* 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)));
}
* 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
* @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.
* @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 -> {
});
}
* @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);
}
* @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
* @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;
}
import java.util.Objects;
import java.util.Optional;
+import java.util.function.BiFunction;
import java.util.function.Consumer;
import java.util.function.Function;
}
}
+ @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");
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;
}
@Test
- public void withStatusChangeHandlerExample() {
+ public void withBindingStatusChangeHandlerExample() {
Label nameStatus = new Label();
AtomicReference<ValidationStatusChangeEvent> event = new AtomicReference<>();
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());
+
+ }
}
Assert.assertNull(event.get());
event.set(evt);
});
- binding.bind(Person::getFirstName, Person::setLastName);
+ binding.bind(Person::getFirstName, Person::setFirstName);
nameField.setValue("");
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());
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("");
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());
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());
}
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);
}
Label label = new Label();
Binding<Person, String, String> binding = binder.forField(nameField);
- binding.bind(Person::getFirstName, Person::setLastName);
binding.withStatusChangeHandler(event -> {
});
Label label = new Label();
Binding<Person, String, String> binding = binder.forField(nameField);
- binding.bind(Person::getFirstName, Person::setLastName);
binding.withStatusLabel(label);
public void bingingWithStatusChangeHandler_setAfterOtherHandler() {
Binding<Person, String, String> binding = binder.forField(nameField);
- binding.bind(Person::getFirstName, Person::setLastName);
binding.withStatusChangeHandler(event -> {
});
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());
+ }
+
}