@Override
public <FIELDVALUE> BeanBinding<BEAN, FIELDVALUE, FIELDVALUE> forField(
HasValue<FIELDVALUE> field) {
- return createBinding(field, Converter.identity(),
- this::handleValidationStatus);
+ return (BeanBinding<BEAN, FIELDVALUE, FIELDVALUE>) super.forField(
+ field);
}
/**
exception -> errorMessage));
}
+ /**
+ * Maps binding value {@code null} to given null representation and back
+ * to {@code null} when converting back to model value.
+ *
+ * @param nullRepresentation
+ * the value to use instead of {@code null}
+ * @return a new binding with null representation handling.
+ */
+ public default Binding<BEAN, FIELDVALUE, TARGET> withNullRepresentation(
+ TARGET nullRepresentation) {
+ return withConverter(
+ fieldValue -> Objects.equals(fieldValue, nullRepresentation)
+ ? null : fieldValue,
+ modelValue -> Objects.isNull(modelValue)
+ ? nullRepresentation : modelValue);
+ }
+
/**
* Gets the field the binding uses.
*
return validationStatus;
}
- private void setBeanValue(BEAN bean, TARGET value) {
- setter.accept(bean, value);
- }
-
private void notifyStatusHandler(ValidationStatus<?> status) {
statusHandler.accept(status);
}
* {@link Binding#bind(Function, BiConsumer) Binding.bind} which completes
* the binding. Until {@code Binding.bind} is called, the binding has no
* effect.
+ * <p>
+ * <strong>Note:</strong> Not all {@link HasValue} implementations support
+ * passing {@code null} as the value. For these the Binder will
+ * automatically change {@code null} to a null representation provided by
+ * {@link HasValue#getEmptyValue()}. This conversion is one-way only, if you
+ * want to have a two-way mapping back to {@code null}, use
+ * {@link Binding#withNullRepresentation(Object))}.
*
* @param <FIELDVALUE>
* the value type of the field
clearError(field);
getStatusLabel().ifPresent(label -> label.setValue(""));
- return createBinding(field, Converter.identity(),
+ return createBinding(field, Converter.from(fieldValue -> fieldValue,
+ modelValue -> Objects.isNull(modelValue) ? field.getEmptyValue()
+ : modelValue,
+ exception -> exception.getMessage()),
this::handleValidationStatus);
}
* Use the {@link #forField(HasValue)} overload instead if you want to
* further configure the new binding.
* <p>
+ * <strong>Note:</strong> Not all {@link HasValue} implementations support
+ * passing {@code null} as the value. For these the Binder will
+ * automatically change {@code null} to a null representation provided by
+ * {@link HasValue#getEmptyValue()}. This conversion is one-way only, if you
+ * want to have a two-way mapping back to {@code null}, use
+ * {@link #forField(HasValue)} and
+ * {@link Binding#withNullRepresentation(Object))}.
+ * <p>
* When a bean is bound with {@link Binder#bind(BEAN)}, the field value is
* set to the return value of the given getter. The property value is then
* updated via the given setter whenever the field value changes. The setter
// Save old bean values so we can restore them if validators fail
Map<Binding<BEAN, ?, ?>, Object> oldValues = new HashMap<>();
- bindings.forEach(binding -> oldValues.put(binding,
- binding.convertDataToFieldType(bean)));
+ bindings.forEach(
+ binding -> oldValues.put(binding, binding.getter.apply(bean)));
bindings.forEach(binding -> binding.storeFieldValue(bean));
// Now run bean level validation against the updated bean
.findAny().isPresent();
if (hasErrors) {
// Bean validator failed, revert values
- bindings.forEach((BindingImpl binding) -> binding.setBeanValue(bean,
- oldValues.get(binding)));
+ bindings.forEach((BindingImpl binding) -> binding.setter
+ .accept(bean, oldValues.get(binding)));
} else {
// Save successful, reset hasChanges to false
setHasChanges(false);
import java.io.Serializable;
import java.lang.reflect.Method;
+import java.util.Objects;
+import java.util.function.BiConsumer;
+import java.util.function.Function;
import com.vaadin.event.ConnectorEvent;
import com.vaadin.event.EventListener;
*/
public Registration addValueChangeListener(
ValueChangeListener<? super V> listener);
+
+ /**
+ * Returns the value that represents an empty value.
+ * <p>
+ * By default {@link HasValue} is expected to support {@code null} as empty
+ * values. Specific implementations might not support this.
+ *
+ * @return empty value
+ * @see Binder#bind(HasValue, Function, BiConsumer)
+ */
+ public default V getEmptyValue() {
+ return null;
+ }
+
+ /**
+ * Returns whether this {@code HasValue} is considered to be empty.
+ * <p>
+ * By default this is an equality check between current value and empty
+ * value.
+ *
+ * @return {@code true} if considered empty; {@code false} if not
+ */
+ public default boolean isEmpty() {
+ return Objects.equals(getValue(), getEmptyValue());
+ }
}
this.color = color;
getState().color = color.getCSS();
}
+
+ @Override
+ public Color getEmptyValue() {
+ return Color.WHITE;
+ }
}
* The call is delegated to {@link #getSelectedItems()}
*
* @return the current selection
- *
+ *
* @see #getSelectedItems()
* @see SelectionModel#getSelectedItems
*/
new LinkedHashSet<>(getSelectionModel().getSelectedItems()));
}
+ @Override
+ public Set<T> getEmptyValue() {
+ return Collections.emptySet();
+ }
+
/**
* Adds a value change listener. The listener is called when the selection
* set of this multi select is changed either by the user or
* programmatically.
- *
+ *
* @see #addSelectionListener(MultiSelectionListener)
*
* @param listener
package com.vaadin.ui;
import java.util.Collection;
+import java.util.Objects;
import org.jsoup.nodes.Attributes;
import org.jsoup.nodes.Element;
@Override
public void setValue(String value) {
- if (value == null) {
- setValue("", false);
- } else {
- setValue(value, false);
- }
+ Objects.requireNonNull(value, "Null value not supported");
+ setValue(value, false);
}
/**
setValue("");
}
- /**
- * Checks if the field is empty.
- *
- * @return <code>true</code> if the field value is an empty string,
- * <code>false</code> otherwise
- */
- public boolean isEmpty() {
- return "".equals(getValue());
- }
-
@Override
public void writeDesign(Element design, DesignContext designContext) {
super.writeDesign(design, designContext);
customAttributes.add("cursor-position");
return customAttributes;
}
+
+ @Override
+ public String getEmptyValue() {
+ return "";
+ }
}
super.setValue(value);
}
+ @Override
+ public Boolean getEmptyValue() {
+ return false;
+ }
+
@Override
protected CheckBoxState getState() {
return (CheckBoxState) super.getState();
return getState(false).value;
}
+ @Override
+ public String getEmptyValue() {
+ return "";
+ }
+
@Override
protected void doSetValue(String value) {
getState().value = value;
return getState(false).valueChangeTimeout;
}
- /**
- * Checks if the field is empty.
- *
- * @return <code>true</code> if the field value is an empty string,
- * <code>false</code> otherwise
- */
- public boolean isEmpty() {
- return getValue().length() == 0;
- }
-
/**
* Clears the value of this field.
*/
return getState().value;
}
+ @Override
+ public Double getEmptyValue() {
+ return getMin();
+ }
+
/**
* Thrown when the value of the slider is about to be set to a value that is
* outside the valid range of the slider.
--- /dev/null
+package com.vaadin.data;
+
+import java.util.Collections;
+
+import org.junit.Assert;
+import org.junit.Before;
+import org.junit.Test;
+
+import com.vaadin.shared.ui.colorpicker.Color;
+import com.vaadin.ui.CheckBox;
+import com.vaadin.ui.CheckBoxGroup;
+import com.vaadin.ui.ColorPicker;
+import com.vaadin.ui.RichTextArea;
+import com.vaadin.ui.Slider;
+
+public class BinderComponentTest
+ extends BinderTestBase<Binder<String>, String> {
+
+ enum TestValues {
+ TRUE, FALSE, FILE_NOT_FOUND
+ }
+
+ @Before
+ public void setUp() {
+ binder = new Binder<>();
+ item = "Foo";
+ }
+
+ @Test
+ public void slider_bind_null() {
+ double minValue = 10.5d;
+ double initialValue = 28.2d;
+
+ Slider slider = new Slider();
+ slider.setResolution(1);
+ slider.setMin(minValue);
+
+ testFieldNullRepresentation(initialValue, slider);
+ }
+
+ @Test
+ public void colorpicker_bind_null() {
+ testFieldNullRepresentation(new Color(123, 254, 213),
+ new ColorPicker());
+ }
+
+ @Test
+ public void richtextarea_bind_null() {
+ testFieldNullRepresentation("Test text", new RichTextArea());
+ }
+
+ @Test
+ public void checkbox_bind_null() {
+ testFieldNullRepresentation(true, new CheckBox());
+ }
+
+ @Test
+ public void checkboxgroup_bind_null() {
+ CheckBoxGroup<TestValues> checkBoxGroup = new CheckBoxGroup<>();
+ checkBoxGroup.setItems(TestValues.values());
+ testFieldNullRepresentation(
+ Collections.singleton(TestValues.FILE_NOT_FOUND),
+ checkBoxGroup);
+ }
+
+ private <T> void testFieldNullRepresentation(T initialValue,
+ HasValue<T> field) {
+ binder.bind(field, t -> null, (str, val) -> {
+ Assert.assertEquals("Value update with initial value failed.",
+ initialValue, field.getValue());
+ });
+ field.setValue(initialValue);
+ Assert.assertEquals("Initial value of field unexpected", initialValue,
+ field.getValue());
+ binder.bind(item);
+ Assert.assertEquals("Null representation for field failed",
+ field.getEmptyValue(), field.getValue());
+ field.setValue(initialValue);
+ }
+
+}
/*
* 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
import org.junit.Test;
import com.vaadin.data.Binder.Binding;
+import com.vaadin.data.util.converter.StringToIntegerConverter;
import com.vaadin.data.validator.NotEmptyValidator;
import com.vaadin.server.AbstractErrorMessage;
import com.vaadin.server.ErrorMessage;
import com.vaadin.ui.Label;
import com.vaadin.ui.TextField;
-public class BinderConverterValidatorTest extends
- BinderTestBase<Binder<Person>, Person> {
+public class BinderConverterValidatorTest
+ extends BinderTestBase<Binder<Person>, Person> {
private static class StatusBean {
private String status;
.bind(Person::getAge, Person::setAge);
binder.bind(item);
}
+
+ @Test(expected = ValidationException.class)
+ public void save_beanValidationErrorsWithConverter()
+ throws ValidationException {
+ Binder<Person> binder = new Binder<>();
+ binder.forField(ageField)
+ .withConverter(new StringToIntegerConverter("Can't convert"))
+ .bind(Person::getAge, Person::setAge);
+
+ binder.withValidator(Validator.from(person -> false, "b"));
+
+ Person person = new Person();
+ ageField.setValue("1");
+ try {
+ binder.save(person);
+ } finally {
+ // Bean should have been updated for item validation but reverted
+ Assert.assertEquals(0, person.getAge());
+ }
+ }
}
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertSame;
+import java.util.Objects;
+
import org.junit.Assert;
import org.junit.Before;
import org.junit.Test;
import com.vaadin.tests.data.bean.Person;
+import com.vaadin.tests.data.bean.Sex;
+import com.vaadin.ui.TextField;
public class BinderTest extends BinderTestBase<Binder<Person>, Person> {
binder.bind(nameField, Person::getFirstName, Person::setFirstName);
binder.bind(item);
}
-}
+
+ @Test
+ public void binding_with_null_representation() {
+ String nullRepresentation = "Some arbitrary text";
+ String realName = "John";
+ Person namelessPerson = new Person(null, "Doe", "", 25, Sex.UNKNOWN,
+ null);
+
+ binder.forField(nameField).withNullRepresentation(nullRepresentation)
+ .bind(Person::getFirstName, Person::setFirstName);
+
+ // Bind a person with null value and check that null representation is
+ // used
+ binder.bind(namelessPerson);
+ Assert.assertEquals(
+ "Null value from bean was not converted to explicit null representation",
+ nullRepresentation, nameField.getValue());
+
+ // Verify that changes are applied to bean
+ nameField.setValue(realName);
+ Assert.assertEquals(
+ "Bean was not correctly updated from a change in the field",
+ realName, namelessPerson.getFirstName());
+
+ // Verify conversion back to null
+ nameField.setValue(nullRepresentation);
+ Assert.assertEquals(
+ "Two-way null representation did not change value back to null",
+ null, namelessPerson.getFirstName());
+ }
+
+ @Test
+ public void binding_with_default_null_representation() {
+ TextField nullTextField = new TextField() {
+ @Override
+ public String getEmptyValue() {
+ return "null";
+ }
+ };
+
+ Person namelessPerson = new Person(null, "Doe", "", 25, Sex.UNKNOWN,
+ null);
+ binder.bind(nullTextField, Person::getFirstName, Person::setFirstName);
+ binder.bind(namelessPerson);
+
+ Assert.assertTrue(nullTextField.isEmpty());
+ Assert.assertEquals(null, namelessPerson.getFirstName());
+
+ // Change value, see that textfield is not empty and bean is updated.
+ nullTextField.setValue("");
+ Assert.assertFalse(nullTextField.isEmpty());
+ Assert.assertEquals("First name of person was not properly updated", "",
+ namelessPerson.getFirstName());
+
+ // Verify that default null representation does not map back to null
+ nullTextField.setValue("null");
+ Assert.assertTrue(nullTextField.isEmpty());
+ Assert.assertEquals("Default one-way null representation failed.",
+ "null", namelessPerson.getFirstName());
+ }
+
+ @Test
+ public void binding_with_null_representation_value_not_null() {
+ String nullRepresentation = "Some arbitrary text";
+
+ binder.forField(nameField).withNullRepresentation(nullRepresentation)
+ .bind(Person::getFirstName, Person::setFirstName);
+
+ Assert.assertFalse("First name in item should not be null",
+ Objects.isNull(item.getFirstName()));
+ binder.bind(item);
+
+ Assert.assertEquals("Field value was not set correctly",
+ item.getFirstName(), nameField.getValue());
+ }
+}
\ No newline at end of file