/*
* 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.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import java.util.Set;
import java.util.function.BiConsumer;
import java.util.function.Function;
import java.util.function.Predicate;
import java.util.stream.Collectors;
import com.vaadin.data.util.converter.Converter;
import com.vaadin.data.util.converter.StringToIntegerConverter;
import com.vaadin.server.ErrorMessage;
import com.vaadin.server.UserError;
import com.vaadin.shared.Registration;
import com.vaadin.shared.data.selection.SelectionModel.Multi;
import com.vaadin.ui.AbstractComponent;
import com.vaadin.ui.AbstractMultiSelect;
import com.vaadin.ui.AbstractSingleSelect;
import com.vaadin.ui.Component;
import com.vaadin.ui.Label;
import com.vaadin.ui.UI;
/**
* Connects one or more {@code Field} components to properties of a backing data
* type such as a bean type. With a binder, input components can be grouped
* together into forms to easily create and update business objects with little
* explicit logic needed to move data between the UI and the data layers of the
* application.
*
* A binder is a collection of bindings, each representing the mapping of
* a single field, through converters and validators, to a backing property.
*
* A binder instance can be bound to a single bean instance at a time, but can
* be rebound as needed. This allows usage patterns like a master-details
* view, where a select component is used to pick the bean to edit.
*
* Bean level validators can be added using the
* {@link #withValidator(Validator)} method and will be run on the bound bean
* once it has been updated from the values of the bound fields. Bean level
* validators are also run as part of {@link #save(Object)} and
* {@link #saveIfValid(Object)} if all field level validators pass.
*
* Note: For bean level validators, the item must be updated before the
* validators are run. If a bean level validator fails in {@link #save(Object)}
* or {@link #saveIfValid(Object)}, the item will be reverted to the previous
* state before returning from the method. You should ensure that the
* getters/setters in the item do not have side effects.
*
* Unless otherwise specified, {@code Binder} method arguments cannot be null.
*
* @author Vaadin Ltd.
*
* @param
* the bean type
*
* @see Binding
* @see HasValue
*
* @since 8.0
*/
public class Binder implements Serializable {
/**
* Represents the binding between a field and a data property.
*
* @param
* the bean type
* @param
* the value type of the field
* @param
* the target data type of the binding, matches the field type
* until a converter has been set
*
* @see Binder#forField(HasValue)
*/
public interface Binding extends Serializable {
/**
* Completes this binding using the given getter and setter functions
* representing a backing bean property. The functions are used to
* update the field value from the property and to store the field value
* to the property, respectively.
*
* 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 may be null; in that case the property value is never
* updated and the binding is said to be read-only.
*
* If the Binder is already bound to some item, the newly bound field is
* associated with the corresponding bean property as described above.
*
* The getter and setter can be arbitrary functions, for instance
* implementing user-defined conversion or validation. However, in the
* most basic use case you can simply pass a pair of method references
* to this method as follows:
*
*
* class Person {
* public String getName() { ... }
* public void setName(String name) { ... }
* }
*
* TextField nameField = new TextField();
* binder.forField(nameField).bind(Person::getName, Person::setName);
*
*
* @param getter
* the function to get the value of the property to the
* field, not null
* @param setter
* the function to save the field value to the property or
* null if read-only
* @throws IllegalStateException
* if {@code bind} has already been called on this binding
*/
public void bind(Function getter,
BiConsumer setter);
/**
* Adds a validator to this binding. Validators are applied, in
* registration order, when the field value is saved to the backing
* property. If any validator returns a failure, the property value is
* not updated.
*
* @param validator
* the validator to add, not null
* @return this binding, for chaining
* @throws IllegalStateException
* if {@code bind} has already been called
*/
public Binding withValidator(
Validator super TARGET> validator);
/**
* A convenience method to add a validator to this binding using the
* {@link Validator#from(Predicate, String)} factory method.
*
* Validators are applied, in registration order, when the field value
* is saved to the backing property. If any validator returns a failure,
* the property value is not updated.
*
* @see #withValidator(Validator)
* @see Validator#from(Predicate, String)
*
* @param predicate
* the predicate performing validation, not null
* @param message
* the error message to report in case validation failure
* @return this binding, for chaining
* @throws IllegalStateException
* if {@code bind} has already been called
*/
public default Binding withValidator(
Predicate super TARGET> predicate, String message) {
return withValidator(Validator.from(predicate, message));
}
/**
* Maps the binding to another data type using the given
* {@link Converter}.
*
* A converter is capable of converting between a presentation type,
* which must match the current target data type of the binding, and a
* model type, which can be any data type and becomes the new target
* type of the binding. When invoking
* {@link #bind(Function, BiConsumer)}, the target type of the binding
* must match the getter/setter types.
*
* For instance, a {@code TextField} can be bound to an integer-typed
* property using an appropriate converter such as a
* {@link StringToIntegerConverter}.
*
* @param
* the type to convert to
* @param converter
* the converter to use, not null
* @return a new binding with the appropriate type
* @throws IllegalStateException
* if {@code bind} has already been called
*/
public Binding withConverter(
Converter converter);
/**
* Maps the binding to another data type using the mapping functions and
* a possible exception as the error message.
*
* The mapping functions are used to convert between a presentation
* type, which must match the current target data type of the binding,
* and a model type, which can be any data type and becomes the new
* target type of the binding. When invoking
* {@link #bind(Function, BiConsumer)}, the target type of the binding
* must match the getter/setter types.
*
* For instance, a {@code TextField} can be bound to an integer-typed
* property using appropriate functions such as:
* withConverter(Integer::valueOf, String::valueOf);
*
* @param
* the type to convert to
* @param toModel
* the function which can convert from the old target type to
* the new target type
* @param toPresentation
* the function which can convert from the new target type to
* the old target type
* @return a new binding with the appropriate type
* @throws IllegalStateException
* if {@code bind} has already been called
*/
public default Binding withConverter(
Function toModel,
Function toPresentation) {
return withConverter(Converter.from(toModel, toPresentation,
exception -> exception.getMessage()));
}
/**
* Maps the binding to another data type using the mapping functions and
* the given error error message if a value cannot be converted to the
* new target type.
*
* The mapping functions are used to convert between a presentation
* type, which must match the current target data type of the binding,
* and a model type, which can be any data type and becomes the new
* target type of the binding. When invoking
* {@link #bind(Function, BiConsumer)}, the target type of the binding
* must match the getter/setter types.
*
* For instance, a {@code TextField} can be bound to an integer-typed
* property using appropriate functions such as:
* withConverter(Integer::valueOf, String::valueOf);
*
* @param
* the type to convert to
* @param toModel
* the function which can convert from the old target type to
* the new target type
* @param toPresentation
* the function which can convert from the new target type to
* the old target type
* @param errorMessage
* the error message to use if conversion using
* toModel fails
* @return a new binding with the appropriate type
* @throws IllegalStateException
* if {@code bind} has already been called
*/
public default Binding withConverter(
Function toModel,
Function toPresentation,
String errorMessage) {
return withConverter(Converter.from(toModel, toPresentation,
exception -> errorMessage));
}
/**
* Gets the field the binding uses.
*
* @return the field for the binding
*/
public HasValue getField();
/**
* Sets the given {@code label} to show an error message if validation
* fails.
*
* The validation state of each field is updated whenever the user
* modifies the value of that field. The validation state is by default
* shown using {@link AbstractComponent#setComponentError} which is used
* by the layout that the field is shown in. Most built-in layouts will
* show this as a red exclamation mark icon next to the component, so
* that hovering or tapping the icon shows a tooltip with the message
* text.
*
* This method allows to customize the way a binder displays error
* messages to get more flexibility than what
* {@link AbstractComponent#setComponentError} provides (it replaces the
* default behavior).
*
* This is just a shorthand for
* {@link #withValidationStatusHandler(ValidationStatusHandler)} method
* where the handler instance hides the {@code label} if there is no
* error and shows it with validation error message if validation fails.
* It means that it cannot be called after
* {@link #withValidationStatusHandler(ValidationStatusHandler)} method
* call or {@link #withValidationStatusHandler(ValidationStatusHandler)}
* after this method call.
*
* @see #withValidationStatusHandler(ValidationStatusHandler)
* @see AbstractComponent#setComponentError(ErrorMessage)
* @param label
* label to show validation status for the field
* @return this binding, for chaining
*/
public default Binding withStatusLabel(
Label label) {
return withValidationStatusHandler(status -> {
label.setValue(status.getMessage().orElse(""));
// Only show the label when validation has failed
label.setVisible(status.isError());
});
}
/**
* Sets a {@link ValidationStatusHandler} to track validation status
* changes.
*
* The validation state of each field is updated whenever the user
* modifies the value of that field. The validation state is by default
* shown using {@link AbstractComponent#setComponentError} which is used
* by the layout that the field is shown in. Most built-in layouts will
* show this as a red exclamation mark icon next to the component, so
* that hovering or tapping the icon shows a tooltip with the message
* text.
*
* This method allows to customize the way a binder displays error
* messages to get more flexibility than what
* {@link AbstractComponent#setComponentError} provides (it replaces the
* default behavior).
*
* The method may be called only once. It means there is no chain unlike
* {@link #withValidator(Validator)} or
* {@link #withConverter(Converter)}. Also it means that the shorthand
* method {@link #withStatusLabel(Label)} also may not be called after
* this method.
*
* @see #withStatusLabel(Label)
* @see AbstractComponent#setComponentError(ErrorMessage)
* @param handler
* status change handler
* @return this binding, for chaining
*/
public Binding withValidationStatusHandler(
ValidationStatusHandler handler);
/**
* Validates the field value and returns a {@code ValidationStatus}
* instance representing the outcome of the validation.
*
* @see Binder#validate()
* @see Validator#apply(Object)
*
* @return the validation result.
*/
public ValidationStatus validate();
}
/**
* An internal implementation of {@code Binding}.
*
* @param
* the bean type, must match the Binder bean type
* @param
* the value type of the field
* @param
* the target data type of the binding, matches the field type
* until a converter has been set
*/
protected static class BindingImpl
implements Binding {
private final Binder binder;
private final HasValue field;
private Registration onValueChange;
private ValidationStatusHandler statusHandler;
private boolean isStatusHandlerChanged;
private Function getter;
private BiConsumer setter;
/**
* Contains all converters and validators chained together in the
* correct order.
*/
private Converter converterValidatorChain;
/**
* Creates a new binding associated with the given field. Initializes
* the binding with the given converter chain and status change handler.
*
* @param binder
* the binder this instance is connected to, not null
* @param field
* the field to bind, not null
* @param converterValidatorChain
* the converter/validator chain to use, not null
* @param statusHandler
* the handler to track validation status, not null
*/
protected BindingImpl(Binder binder, HasValue field,
Converter converterValidatorChain,
ValidationStatusHandler statusHandler) {
this.field = field;
this.binder = binder;
this.converterValidatorChain = converterValidatorChain;
this.statusHandler = statusHandler;
}
@Override
public void bind(Function getter,
BiConsumer setter) {
checkUnbound();
Objects.requireNonNull(getter, "getter cannot be null");
this.getter = getter;
this.setter = setter;
getBinder().bindings.add(this);
getBinder().getBean().ifPresent(this::bind);
}
@Override
public Binding withValidator(
Validator super TARGET> validator) {
checkUnbound();
Objects.requireNonNull(validator, "validator cannot be null");
converterValidatorChain = converterValidatorChain
.chain(new ValidatorAsConverter<>(validator));
return this;
}
@Override
public Binding withConverter(
Converter converter) {
checkUnbound();
Objects.requireNonNull(converter, "converter cannot be null");
return getBinder().createBinding(getField(),
converterValidatorChain.chain(converter), statusHandler);
}
@Override
public Binding withValidationStatusHandler(
ValidationStatusHandler handler) {
checkUnbound();
Objects.requireNonNull(handler, "handler cannot be null");
if (isStatusHandlerChanged) {
throw new IllegalStateException(
"A " + ValidationStatusHandler.class.getSimpleName()
+ " has already been set");
}
isStatusHandlerChanged = true;
statusHandler = handler;
return this;
}
@Override
public HasValue getField() {
return field;
}
/**
* Returns the {@code Binder} connected to this {@code Binding}
* instance.
*
* @return the binder
*/
protected Binder getBinder() {
return binder;
}
/**
* Throws if this binding is already completed and cannot be modified
* anymore.
*
* @throws IllegalStateException
* if this binding is already bound
*/
protected void checkUnbound() {
if (getter != null) {
throw new IllegalStateException(
"cannot modify binding: already bound to a property");
}
}
/**
* Finds an appropriate locale to be used in conversion and validation.
*
* @return the found locale, not null
*/
protected Locale findLocale() {
Locale l = null;
if (getField() instanceof Component) {
l = ((Component) getField()).getLocale();
}
if (l == null && UI.getCurrent() != null) {
l = UI.getCurrent().getLocale();
}
if (l == null) {
l = Locale.getDefault();
}
return l;
}
private void bind(BEAN bean) {
setFieldValue(bean);
onValueChange = getField()
.addValueChangeListener(e -> handleFieldValueChange(bean));
}
@Override
public ValidationStatus validate() {
ValidationStatus status = doValidation();
getBinder().getValidationStatusHandler()
.accept(new BinderValidationStatus<>(getBinder(),
Arrays.asList(status), Collections.emptyList()));
return status;
}
/**
* Returns the field value run through all converters and validators,
* but doesn't pass the {@link ValidationStatus} to any status handler.
*
* @return the validation status
*/
private ValidationStatus doValidation() {
FIELDVALUE fieldValue = field.getValue();
Result dataValue = converterValidatorChain.convertToModel(
fieldValue, findLocale());
return new ValidationStatus<>(this, dataValue);
}
private void unbind() {
onValueChange.remove();
}
/**
* Sets the field value by invoking the getter function on the given
* bean.
*
* @param bean
* the bean to fetch the property value from
*/
private void setFieldValue(BEAN bean) {
assert bean != null;
getField().setValue(convertDataToFieldType(bean));
}
private FIELDVALUE convertDataToFieldType(BEAN bean) {
return converterValidatorChain.convertToPresentation(
getter.apply(bean), findLocale());
}
/**
* Handles the value change triggered by the bound field.
*
* @param bean
* the new value
*/
private void handleFieldValueChange(BEAN bean) {
binder.setHasChanges(true);
// store field value if valid
ValidationStatus fieldValidationStatus = storeFieldValue(
bean);
List> binderValidationResults;
// if all field level validations pass, run bean level validation
if (!getBinder().bindings.stream().map(BindingImpl::doValidation)
.anyMatch(ValidationStatus::isError)) {
binderValidationResults = binder.validateItem(bean);
} else {
binderValidationResults = Collections.emptyList();
}
binder.getValidationStatusHandler()
.accept(new BinderValidationStatus<>(binder,
Arrays.asList(fieldValidationStatus),
binderValidationResults));
}
/**
* Saves the field value by invoking the setter function on the given
* bean, if the value passes all registered validators.
*
* @param bean
* the bean to set the property value to
*/
private ValidationStatus storeFieldValue(BEAN bean) {
assert bean != null;
ValidationStatus validationStatus = doValidation();
if (setter != null) {
validationStatus.getResult().ifPresent(result -> result
.ifOk(value -> setter.accept(bean, value)));
}
return validationStatus;
}
private void setBeanValue(BEAN bean, TARGET value) {
setter.accept(bean, value);
}
private void notifyStatusHandler(ValidationStatus> status) {
statusHandler.accept(status);
}
}
/**
* Wraps a validator as a converter.
*
* The type of the validator must be of the same type as this converter or a
* super type of it.
*
* @param
* the type of the converter
*/
private static class ValidatorAsConverter implements Converter {
private Validator super T> validator;
/**
* Creates a new converter wrapping the given validator.
*
* @param validator
* the validator to wrap
*/
public ValidatorAsConverter(Validator super T> validator) {
this.validator = validator;
}
@Override
public Result convertToModel(T value, Locale locale) {
Result super T> validationResult = validator.apply(value);
if (validationResult.isError()) {
return Result.error(validationResult.getMessage().get());
} else {
return Result.ok(value);
}
}
@Override
public T convertToPresentation(T value, Locale locale) {
return value;
}
}
private BEAN bean;
private final Set> bindings = new LinkedHashSet<>();
private final List> validators = new ArrayList<>();
private Label statusLabel;
private BinderValidationStatusHandler statusHandler;
private boolean hasChanges = false;
/**
* 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.
*
* @return the currently bound bean if any
*/
public Optional getBean() {
return Optional.ofNullable(bean);
}
/**
* Creates a new binding for the given field. The returned binding may be
* further configured before invoking
* {@link Binding#bind(Function, BiConsumer) Binding.bind} which completes
* the binding. Until {@code Binding.bind} is called, the binding has no
* effect.
*
* @param
* the value type of the field
* @param field
* the field to be bound, not null
* @return the new binding
*
* @see #bind(HasValue, Function, BiConsumer)
*/
public Binding forField(
HasValue field) {
Objects.requireNonNull(field, "field cannot be null");
// clear previous errors for this field and any bean level validation
clearError(field);
getStatusLabel().ifPresent(label -> label.setValue(""));
return createBinding(field, Converter.identity(),
this::handleValidationStatus);
}
/**
* Creates a new binding for the given single select component. The returned
* binding may be further configured before invoking
* {@link Binding#bind(Function, BiConsumer) Binding.bind} which completes
* the binding. Until {@code Binding.bind} is called, the binding has no
* effect.
*
* @param
* the item type of the select
* @param select
* the select to be bound, not null
* @return the new binding
*
* @see #bind(AbstractSingleSelect, Function, BiConsumer)
*/
public Binding forSelect(
AbstractSingleSelect select) {
return forField(new HasValue() {
@Override
public void setValue(SELECTVALUE value) {
select.setSelectedItem(value);
}
@Override
public SELECTVALUE getValue() {
return select.getSelectedItem().orElse(null);
}
@Override
public Registration addValueChangeListener(
ValueChangeListener super SELECTVALUE> listener) {
return select.addSelectionListener(e -> listener.accept(
new ValueChange<>(select, getValue(), e
.isUserOriginated())));
}
});
}
/**
* Creates a new binding for the given multi select component. The returned
* binding may be further configured before invoking
* {@link Binding#bind(Function, BiConsumer) Binding.bind} which completes
* the binding. Until {@code Binding.bind} is called, the binding has no
* effect.
*
* @param
* the item type of the select
* @param select
* the select to be bound, not null
* @return the new binding
*/
public Binding, Set> forSelect(
AbstractMultiSelect select) {
return forField(new HasValue>() {
@Override
public void setValue(Set value) {
Multi selectionModel = select.getSelectionModel();
selectionModel.deselectAll();
value.forEach(selectionModel::select);
}
@Override
public Set getValue() {
return select.getSelectionModel().getSelectedItems();
}
@Override
public Registration addValueChangeListener(
ValueChangeListener super Set> listener) {
return select.addSelectionListener(
e -> listener.accept(new ValueChange<>(select,
getValue(), e.isUserOriginated())));
}
});
}
/**
* Binds a field to a bean property represented by the given getter and
* setter pair. The functions are used to update the field value from the
* property and to store the field value to the property, respectively.
*