com.vaadin.data.Binder Maven / Gradle / Ivy
/*
* Copyright (C) 2000-2024 Vaadin Ltd
*
* This program is available under Vaadin Commercial License and Service Terms.
*
* See for the full
* license.
*/
package com.vaadin.data;
import java.io.Serializable;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Type;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.IdentityHashMap;
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.BiFunction;
import java.util.logging.Logger;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import com.googlecode.gentyref.GenericTypeReflector;
import com.vaadin.annotations.PropertyId;
import com.vaadin.data.HasValue.ValueChangeEvent;
import com.vaadin.data.HasValue.ValueChangeListener;
import com.vaadin.data.converter.ConverterFactory;
import com.vaadin.data.converter.DefaultConverterFactory;
import com.vaadin.data.converter.StringToIntegerConverter;
import com.vaadin.data.validator.BeanValidator;
import com.vaadin.event.EventRouter;
import com.vaadin.server.AbstractErrorMessage.ContentMode;
import com.vaadin.server.ErrorMessage;
import com.vaadin.server.SerializableBiPredicate;
import com.vaadin.server.SerializableConsumer;
import com.vaadin.server.SerializableFunction;
import com.vaadin.server.SerializablePredicate;
import com.vaadin.server.Setter;
import com.vaadin.server.UserError;
import com.vaadin.shared.Registration;
import com.vaadin.shared.ui.ErrorLevel;
import com.vaadin.ui.AbstractComponent;
import com.vaadin.ui.Component;
import com.vaadin.ui.Label;
import com.vaadin.ui.UI;
import com.vaadin.util.ReflectTools;
/**
* 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 #writeBean(Object)} and
* {@link #writeBeanIfValid(Object)} if all field level validators pass.
*
* Note: For bean level validators, the bean must be updated before the
* validators are run. If a bean level validator fails in
* {@link #writeBean(Object)} or {@link #writeBeanIfValid(Object)}, the bean
* will be reverted to the previous state before returning from the method. You
* should ensure that the getters/setters in the bean do not have side effects.
*
* Unless otherwise specified, {@code Binder} method arguments cannot be null.
*
* @author Vaadin Ltd.
*
* @param
* the bean type
*
* @see BindingBuilder
* @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 target data type of the binding, matches the field type
* unless a converter has been set
*
* @see Binder#forField(HasValue)
*/
public interface Binding extends Serializable {
/**
* Gets the field the binding uses.
*
* @return the field for the binding
*/
public HasValue> getField();
/**
* Validates the field value and returns a {@code ValidationStatus}
* instance representing the outcome of the validation. This method is a
* short-hand for calling {@link #validate(boolean)} with
* {@code fireEvent} {@code true}.
*
* @see #validate(boolean)
* @see Binder#validate()
* @see Validator#apply(Object, ValueContext)
*
* @return the validation result.
*/
public default BindingValidationStatus validate() {
return validate(true);
}
/**
* Validates the field value and returns a {@code ValidationStatus}
* instance representing the outcome of the validation.
*
* Note: Calling this method will not trigger the value
* update in the bean automatically. This method will attempt to
* temporarily apply all current changes to the bean and run full bean
* validation for it. The changes are reverted after bean validation.
*
* @see #validate()
* @see Binder#validate()
*
* @param fireEvent
* {@code true} to fire status event; {@code false} to not
* @return the validation result.
*
* @since 8.2
*/
public BindingValidationStatus validate(boolean fireEvent);
/**
* Gets the validation status handler for this Binding.
*
* @return the validation status handler for this binding
*
* @since 8.2
*/
public BindingValidationStatusHandler getValidationStatusHandler();
/**
* Unbinds the binding from its respective {@code Binder} Removes any
* {@code ValueChangeListener} {@code Registration} from associated
* {@code HasValue}.
*
* @since 8.2
*/
public void unbind();
/**
* Reads the value from given item and stores it to the bound field.
*
* @param bean
* the bean to read from
*
* @since 8.2
*/
public void read(BEAN bean);
/**
* Sets the read-only status on for this Binding. Setting a Binding
* read-only will mark the field read-only and not write any values from
* the fields to the bean.
*
* This helper method is the preferred way to control the read-only
* state of the bound field.
*
* @param readOnly
* {@code true} to set binding read-only; {@code false} to
* enable writes
* @since 8.4
* @throws IllegalStateException
* if trying to make binding read-write and the setter is
* {@code null}
*/
public void setReadOnly(boolean readOnly);
/**
* Gets the current read-only status for this Binding.
*
* @see #setReadOnly(boolean)
*
* @return {@code true} if read-only; {@code false} if not
* @since 8.4
*/
public boolean isReadOnly();
/**
* Gets the getter associated with this Binding.
*
* @return the getter
* @since 8.4
*/
public ValueProvider getGetter();
/**
* Gets the setter associated with this Binding.
*
* @return the setter
* @since 8.4
*/
public Setter getSetter();
/**
* Enable or disable asRequired validator. The validator is enabled by
* default.
*
* @see BindingBuilder#asRequired(String)
* @see BindingBuilder#asRequired(ErrorMessageProvider)
*
* @param asRequiredEnabled
* {@code false} if asRequired validator should be disabled,
* {@code true} otherwise (default)
*
* @since 8.10
*/
public void setAsRequiredEnabled(boolean asRequiredEnabled);
/**
* Returns whether asRequired validator is currently enabled or not.
*
* @see BindingBuilder#asRequired(String)
* @see BindingBuilder#asRequired(ErrorMessageProvider)
*
* @return {@code false} if asRequired validator is disabled
* {@code true} otherwise (default)
*
* @since 8.10
*/
public boolean isAsRequiredEnabled();
/**
* Define whether validators are disabled or enabled for this specific
* binding.
*
* @param validatorsDisabled
* A boolean value
*
* @since 8.11
*/
public void setValidatorsDisabled(boolean validatorsDisabled);
/**
* Returns if validators are currently disabled or not
*
* @return A boolean value
*
* @since 8.11
*/
public boolean isValidatorsDisabled();
/**
* Define whether the value should be converted back to the presentation
* in the field when a converter is used in binding.
*
* @param convertBackToPresentation
* A boolean value
*
* @since 8.13
*/
public void setConvertBackToPresentation(
boolean convertBackToPresentation);
/**
* Returns whether the value is converted back to the presentation in
* the field when a converter is used in binding.
*
* @return A boolean value
*
* @since 8.13
*/
public boolean isConvertBackToPresentation();
/**
* Checks whether the field that the binding uses has uncommitted
* changes.
*
* @throws IllegalStateException
* if the binding is no longer attached to a Binder.
*
* @return {@code true} if the field the binding uses has uncommitted
* changes, otherwise {@code false}.
*/
boolean hasChanges();
/**
* Used in comparison of the current value of a field with its initial
* value.
*
* Once set, the value of the field that binding uses will be compared
* with the initial value for hasChanged.
*
*
* @return the predicate to use for equality comparison
*/
SerializableBiPredicate getEqualityPredicate();
}
/**
* Creates a binding between a field and a data property.
*
* @param
* the bean type
* @param
* the target data type of the binding, matches the field type
* until a converter has been set
*
* @see Binder#forField(HasValue)
*/
public interface BindingBuilder extends Serializable {
/**
* Gets the field the binding is being built for.
*
* @return the field this binding is being built for
*/
public HasValue> getField();
/**
* 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#setBean(Object)}, 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 bean, 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);
*
*
*
* Note: when a {@code null} setter is given the field
* will be marked as read-only by invoking
* {@link HasValue#setReadOnly(boolean)}.
*
* @param getter
* the function to get the value of the property to the
* field, not null
* @param setter
* the function to write the field value to the property or
* null if read-only
* @return the newly created binding
* @throws IllegalStateException
* if {@code bind} has already been called on this binding
*/
public Binding bind(ValueProvider getter,
Setter setter);
/**
* Completes this binding by connecting the field to the property with
* the given name. The getter and setter of the property are looked up
* using a {@link PropertySet}.
*
* For a Binder
created using the
* {@link Binder#Binder(Class)} constructor, introspection will be used
* to find a Java Bean property. If a JSR-303 bean validation
* implementation is present on the classpath, a {@link BeanValidator}
* is also added to the binding.
*
* The property must have an accessible getter method. It need not have
* an accessible setter; in that case the property value is never
* updated and the binding is said to be read-only.
*
*
* Note: when the binding is read-only the field
* will be marked as read-only by invoking
* {@link HasValue#setReadOnly(boolean)}.
*
* @param propertyName
* the name of the property to bind, not null
* @return the newly created binding
*
* @throws IllegalArgumentException
* if the property name is invalid
* @throws IllegalArgumentException
* if the property has no accessible getter
* @throws IllegalStateException
* if the binder is not configured with an appropriate
* {@link PropertySet}
*
* @see Binder.BindingBuilder#bind(ValueProvider, Setter)
*/
public Binding bind(String propertyName);
/**
* Adds a validator to this binding. Validators are applied, in
* registration order, when the field value is written to the backing
* property. If any validator returns a failure, the property value is
* not updated.
*
* @see #withValidator(SerializablePredicate, String)
* @see #withValidator(SerializablePredicate, ErrorMessageProvider)
*
* @param validator
* the validator to add, not null
* @return this binding, for chaining
* @throws IllegalStateException
* if {@code bind} has already been called
*/
public BindingBuilder withValidator(
Validator super TARGET> validator);
/**
* A convenience method to add a validator to this binding using the
* {@link Validator#from(SerializablePredicate, String)} factory method.
*
* Validators are applied, in registration order, when the field value
* is written to the backing property. If any validator returns a
* failure, the property value is not updated.
*
* @see #withValidator(Validator)
* @see #withValidator(SerializablePredicate, String, ErrorLevel)
* @see #withValidator(SerializablePredicate, ErrorMessageProvider)
* @see Validator#from(SerializablePredicate, 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 BindingBuilder withValidator(
SerializablePredicate super TARGET> predicate,
String message) {
return withValidator(Validator.from(predicate, message));
}
/**
* A convenience method to add a validator to this binding using the
* {@link Validator#from(SerializablePredicate, String, ErrorLevel)}
* factory method.
*
* Validators are applied, in registration order, when the field value
* is written to the backing property. If any validator returns a
* failure, the property value is not updated.
*
* @see #withValidator(Validator)
* @see #withValidator(SerializablePredicate, String)
* @see #withValidator(SerializablePredicate, ErrorMessageProvider,
* ErrorLevel)
* @see Validator#from(SerializablePredicate, String)
*
* @param predicate
* the predicate performing validation, not null
* @param message
* the error message to report in case validation failure
* @param errorLevel
* the error level for failures from this validator, not null
* @return this binding, for chaining
* @throws IllegalStateException
* if {@code bind} has already been called
*
* @since 8.2
*/
public default BindingBuilder withValidator(
SerializablePredicate super TARGET> predicate, String message,
ErrorLevel errorLevel) {
return withValidator(
Validator.from(predicate, message, errorLevel));
}
/**
* A convenience method to add a validator to this binding using the
* {@link Validator#from(SerializablePredicate, ErrorMessageProvider)}
* factory method.
*
* Validators are applied, in registration order, when the field value
* is written to the backing property. If any validator returns a
* failure, the property value is not updated.
*
* @see #withValidator(Validator)
* @see #withValidator(SerializablePredicate, String)
* @see #withValidator(SerializablePredicate, ErrorMessageProvider,
* ErrorLevel)
* @see Validator#from(SerializablePredicate, ErrorMessageProvider)
*
* @param predicate
* the predicate performing validation, not null
* @param errorMessageProvider
* the provider to generate error messages, not null
* @return this binding, for chaining
* @throws IllegalStateException
* if {@code bind} has already been called
*/
public default BindingBuilder withValidator(
SerializablePredicate super TARGET> predicate,
ErrorMessageProvider errorMessageProvider) {
return withValidator(
Validator.from(predicate, errorMessageProvider));
}
/**
* A convenience method to add a validator to this binding using the
* {@link Validator#from(SerializablePredicate, ErrorMessageProvider, ErrorLevel)}
* factory method.
*
* Validators are applied, in registration order, when the field value
* is written to the backing property. If any validator returns a
* failure, the property value is not updated.
*
* @see #withValidator(Validator)
* @see #withValidator(SerializablePredicate, String, ErrorLevel)
* @see #withValidator(SerializablePredicate, ErrorMessageProvider)
* @see Validator#from(SerializablePredicate, ErrorMessageProvider,
* ErrorLevel)
*
* @param predicate
* the predicate performing validation, not null
* @param errorMessageProvider
* the provider to generate error messages, not null
* @param errorLevel
* the error level for failures from this validator, not null
* @return this binding, for chaining
* @throws IllegalStateException
* if {@code bind} has already been called
*
* @since 8.2
*/
public default BindingBuilder withValidator(
SerializablePredicate super TARGET> predicate,
ErrorMessageProvider errorMessageProvider,
ErrorLevel errorLevel) {
return withValidator(Validator.from(predicate, errorMessageProvider,
errorLevel));
}
/**
* 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(ValueProvider, Setter)}, 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 BindingBuilder 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(ValueProvider, Setter)}, 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 BindingBuilder withConverter(
SerializableFunction toModel,
SerializableFunction 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(ValueProvider, Setter)}, 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 BindingBuilder withConverter(
SerializableFunction toModel,
SerializableFunction toPresentation,
String errorMessage) {
return withConverter(Converter.from(toModel, toPresentation,
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 BindingBuilder withNullRepresentation(
TARGET nullRepresentation) {
return withConverter(
fieldValue -> Objects.equals(fieldValue, nullRepresentation)
? null
: fieldValue,
modelValue -> Objects.isNull(modelValue)
? nullRepresentation
: modelValue);
}
/**
* 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(BindingValidationStatusHandler)}
* 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(BindingValidationStatusHandler)}
* method call or
* {@link #withValidationStatusHandler(BindingValidationStatusHandler)}
* after this method call.
*
* @see #withValidationStatusHandler(BindingValidationStatusHandler)
* @see AbstractComponent#setComponentError(ErrorMessage)
* @param label
* label to show validation status for the field
* @return this binding, for chaining
*/
public default BindingBuilder 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 BindingValidationStatusHandler} 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 BindingBuilder withValidationStatusHandler(
BindingValidationStatusHandler handler);
/**
* Sets the field to be required. This means two things:
*
* - the required indicator will be displayed for this field
* - the field value is validated for not being empty, i.e. that the
* field's value is not equal to what {@link HasValue#getEmptyValue()}
* returns
*
*
* For localizing the error message, use
* {@link #asRequired(ErrorMessageProvider)}.
*
* @see #asRequired(ErrorMessageProvider)
* @see HasValue#setRequiredIndicatorVisible(boolean)
* @see HasValue#isEmpty()
* @param errorMessage
* the error message to show for the invalid value
* @return this binding, for chaining
*/
public default BindingBuilder asRequired(
String errorMessage) {
return asRequired(context -> errorMessage);
}
/**
* Sets the field to be required. This means two things:
*
* - the required indicator will be displayed for this field
* - the field value is validated for not being empty, i.e. that the
* field's value is not equal to what {@link HasValue#getEmptyValue()}
* returns
*
*
* For setting an error message, use {@link #asRequired(String)}.
*
* For localizing the error message, use
* {@link #asRequired(ErrorMessageProvider)}.
*
* @see #asRequired(String)
* @see #asRequired(ErrorMessageProvider)
* @see HasValue#setRequiredIndicatorVisible(boolean)
* @see HasValue#isEmpty()
* @return this binding, for chaining
* @since 8.2
*/
public default BindingBuilder asRequired() {
return asRequired(context -> "");
}
/**
* Sets the field to be required. This means two things:
*
* - the required indicator will be displayed for this field
* - the field value is validated for not being empty, i.e. that the
* field's value is not equal to what {@link HasValue#getEmptyValue()}
* returns
*
*
* @see HasValue#setRequiredIndicatorVisible(boolean)
* @see HasValue#isEmpty()
* @param errorMessageProvider
* the provider for localized validation error message
* @return this binding, for chaining
*/
public BindingBuilder asRequired(
ErrorMessageProvider errorMessageProvider);
/**
* Sets the field to be required and delegates the required check to a
* custom validator. This means two things:
*
* - the required indicator will be displayed for this field
* - the field value is validated by {@code requiredValidator}
*
*
* @see HasValue#setRequiredIndicatorVisible(boolean)
* @param requiredValidator
* validator responsible for the required check
* @return this binding, for chaining
* @since 8.4
*/
public BindingBuilder asRequired(
Validator requiredValidator);
/**
* Sets the {@code equalityPredicate} used to compare the current value
* of a field with its initial value.
*
* By default it is {@literal null}, meaning the initial value
* comparison is not active. Once it is set, the value of the field will
* be compared with its initial value. If the value of the field is set
* back to its initial value, it will not be considered as having
* uncommitted changes.
*
*
* @param equalityPredicate
* the predicate to use for equality comparison
* @return this {@code BindingBuilder}, for method chaining
*/
public default BindingBuilder withEqualityPredicate(
SerializableBiPredicate equalityPredicate) {
return this;
}
}
/**
* An internal implementation of {@code BindingBuilder}.
*
* @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 BindingBuilderImpl
implements BindingBuilder {
private Binder binder;
private final HasValue field;
private BindingValidationStatusHandler statusHandler;
private boolean isStatusHandlerChanged;
private Binding binding;
private boolean bound;
/**
* Contains all converters and validators chained together in the
* correct order.
*/
private Converter converterValidatorChain;
private boolean asRequiredSet;
/**
* A predicate used to compare the current value of a field with its
* initial value. By default it is {@literal null} meaning that the
* initial value comparison is not active
*/
private SerializableBiPredicate equalityPredicate = null;
/**
* Creates a new binding builder associated with the given field.
* Initializes the builder 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 BindingBuilderImpl(Binder binder,
HasValue field,
Converter converterValidatorChain,
BindingValidationStatusHandler statusHandler) {
this.field = field;
this.binder = binder;
this.converterValidatorChain = converterValidatorChain;
this.statusHandler = statusHandler;
}
@Override
public Binding bind(ValueProvider getter,
Setter setter) {
checkUnbound();
Objects.requireNonNull(getter, "getter cannot be null");
BindingImpl binding = new BindingImpl<>(
this, getter, setter);
// Remove existing binding for same field to avoid potential
// multiple application of converter and value change listeners
List> bindingsToRemove = getBinder().bindings
.stream().filter(registeredBinding -> registeredBinding
.getField() == field)
.collect(Collectors.toList());
if (!bindingsToRemove.isEmpty()) {
bindingsToRemove.forEach(Binding::unbind);
getBinder().bindings.removeAll(bindingsToRemove);
}
getBinder().bindings.add(binding);
if (getBinder().getBean() != null) {
binding.initFieldValue(getBinder().getBean(), true);
}
if (setter == null) {
binding.getField().setReadOnly(true);
}
getBinder().fireStatusChangeEvent(false);
bound = true;
getBinder().incompleteBindings.remove(getField());
this.binding = binding;
return binding;
}
@Override
@SuppressWarnings({ "unchecked", "rawtypes" })
public Binding bind(String propertyName) {
Objects.requireNonNull(propertyName,
"Property name cannot be null");
checkUnbound();
PropertyDefinition definition = getBinder().propertySet
.getProperty(propertyName)
.orElseThrow(() -> new IllegalArgumentException(
"Could not resolve property name " + propertyName
+ " from " + getBinder().propertySet));
ValueProvider getter = definition.getGetter();
Setter setter = definition.getSetter().orElse(null);
if (setter == null) {
getLogger().fine(() -> propertyName
+ " does not have an accessible setter");
}
BindingBuilder finalBinding = withConverter(
createConverter(definition.getType()), false);
finalBinding = getBinder().configureBinding(finalBinding,
definition);
try {
Binding binding = ((BindingBuilder) finalBinding).bind(getter,
setter);
getBinder().boundProperties.put(propertyName, binding);
this.binding = binding;
return binding;
} finally {
getBinder().incompleteMemberFieldBindings.remove(getField());
}
}
@SuppressWarnings("unchecked")
private Converter createConverter(Class> getterType) {
return Converter.from(fieldValue -> getterType.cast(fieldValue),
propertyValue -> (TARGET) propertyValue, exception -> {
throw new RuntimeException(exception);
});
}
@Override
public BindingBuilder withValidator(
Validator super TARGET> validator) {
checkUnbound();
Objects.requireNonNull(validator, "validator cannot be null");
Validator super TARGET> wrappedValidator = ((value, context) -> {
if (getBinder().isValidatorsDisabled() || (binding != null
&& binding.isValidatorsDisabled())) {
return ValidationResult.ok();
} else {
return validator.apply(value, context);
}
});
converterValidatorChain = ((Converter) converterValidatorChain)
.chain(new ValidatorAsConverter<>(wrappedValidator));
return this;
}
@Override
public BindingBuilder withConverter(
Converter converter) {
return withConverter(converter, true);
}
@Override
public BindingBuilder withValidationStatusHandler(
BindingValidationStatusHandler handler) {
checkUnbound();
Objects.requireNonNull(handler, "handler cannot be null");
if (isStatusHandlerChanged) {
throw new IllegalStateException("A "
+ BindingValidationStatusHandler.class.getSimpleName()
+ " has already been set");
}
isStatusHandlerChanged = true;
statusHandler = handler;
return this;
}
@Override
public BindingBuilder asRequired(
ErrorMessageProvider errorMessageProvider) {
return asRequired(Validator.from(
value -> !Objects.equals(value, field.getEmptyValue()),
errorMessageProvider));
}
@Override
public BindingBuilder asRequired(
Validator customRequiredValidator) {
checkUnbound();
asRequiredSet = true;
field.setRequiredIndicatorVisible(true);
return withValidator((value, context) -> {
if (!field.isRequiredIndicatorVisible()) {
return ValidationResult.ok();
} else {
return customRequiredValidator.apply(value, context);
}
});
}
@Override
public BindingBuilder withEqualityPredicate(
SerializableBiPredicate equalityPredicate) {
Objects.requireNonNull(equalityPredicate,
"equality predicate cannot be null");
this.equalityPredicate = equalityPredicate;
return this;
}
/**
* Implements {@link #withConverter(Converter)} method with additional
* possibility to disable (reset) default null representation converter.
*
* The method {@link #withConverter(Converter)} calls this method with
* {@code true} provided as the second argument value.
*
* @see #withConverter(Converter)
*
* @param converter
* the converter to use, not null
* @param resetNullRepresentation
* if {@code true} then default null representation will be
* deactivated (if not yet), otherwise it won't be removed
* @return a new binding with the appropriate type
* @param
* the type to convert to
* @throws IllegalStateException
* if {@code bind} has already been called
*/
protected BindingBuilder withConverter(
Converter converter,
boolean resetNullRepresentation) {
checkUnbound();
Objects.requireNonNull(converter, "converter cannot be null");
if (resetNullRepresentation) {
getBinder().initialConverters.get(field).setIdentity();
}
converterValidatorChain = ((Converter) converterValidatorChain)
.chain(converter);
return (BindingBuilder) this;
}
/**
* 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 (bound) {
throw new IllegalStateException(
"cannot modify binding: already bound to a property");
}
}
@Override
public HasValue getField() {
return field;
}
}
/**
* 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
* unless a converter has been set
*/
protected static class BindingImpl
implements Binding {
private Binder binder;
private HasValue field;
private final BindingValidationStatusHandler statusHandler;
private final ValueProvider getter;
private final Setter setter;
private boolean readOnly;
private Registration onValueChange;
private boolean valueInit = false;
private boolean convertedBack = false;
/**
* Contains all converters and validators chained together in the
* correct order.
*/
private final Converter converterValidatorChain;
private boolean asRequiredSet;
private boolean validatorsDisabled = false;
private boolean convertBackToPresentation = true;
private SerializableBiPredicate equalityPredicate;
private TARGET initialValue;
public BindingImpl(BindingBuilderImpl builder,
ValueProvider getter,
Setter setter) {
binder = builder.getBinder();
field = builder.field;
statusHandler = builder.statusHandler;
asRequiredSet = builder.asRequiredSet;
converterValidatorChain = ((Converter) builder.converterValidatorChain);
equalityPredicate = builder.equalityPredicate;
onValueChange = getField()
.addValueChangeListener(this::handleFieldValueChange);
this.getter = getter;
this.setter = setter;
readOnly = setter == null;
}
@Override
public HasValue getField() {
return field;
}
/**
* 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;
}
@Override
public BindingValidationStatus validate(boolean fireEvent) {
Objects.requireNonNull(binder,
"This Binding is no longer attached to a Binder");
BindingValidationStatus status = doValidation();
if (fireEvent) {
getBinder().getValidationStatusHandler()
.statusChange(new BinderValidationStatus<>(getBinder(),
Arrays.asList(status),
Collections.emptyList()));
getBinder().fireStatusChangeEvent(status.isError());
}
return status;
}
/**
* Removes this binding from its binder and unregisters the
* {@code ValueChangeListener} from any bound {@code HasValue}. It does
* nothing if it is called for an already unbound binding.
*
* @since 8.2
*/
@Override
public void unbind() {
if (onValueChange != null) {
onValueChange.remove();
onValueChange = null;
}
if (binder != null) {
binder.removeBindingInternal(this);
binder = null;
}
field = null;
}
/**
* Returns the field value run through all converters and validators,
* but doesn't pass the {@link BindingValidationStatus} to any status
* handler.
*
* @return the result of the conversion
*/
private Result doConversion() {
FIELDVALUE fieldValue = field.getValue();
return converterValidatorChain.convertToModel(fieldValue,
createValueContext());
}
private BindingValidationStatus toValidationStatus(
Result result) {
return new BindingValidationStatus<>(result, this);
}
/**
* Returns the field value run through all converters and validators,
* but doesn't pass the {@link BindingValidationStatus} to any status
* handler.
*
* @return the validation status
*/
private BindingValidationStatus doValidation() {
return toValidationStatus(doConversion());
}
/**
* Creates a value context from the current state of the binding and its
* field.
*
* @return the value context
*/
protected ValueContext createValueContext() {
if (field instanceof Component) {
return new ValueContext((Component) field, field);
}
return new ValueContext(null, field, findLocale());
}
/**
* Sets the field value by invoking the getter function on the given
* bean.
*
* @param bean
* the bean to fetch the property value from
* @param writeBackChangedValues
* true
if the bean value should be updated if
* the value is different after converting to and from the
* presentation value; false
to avoid updating
* the bean value
*/
private void initFieldValue(BEAN bean, boolean writeBackChangedValues) {
assert bean != null;
valueInit = true;
try {
TARGET originalValue = getter.apply(bean);
convertAndSetFieldValue(originalValue);
if (writeBackChangedValues && setter != null && !readOnly) {
doConversion().ifOk(convertedValue -> {
if (!Objects.equals(originalValue, convertedValue)) {
setter.accept(bean, convertedValue);
}
});
}
} finally {
valueInit = false;
}
}
private FIELDVALUE convertToFieldType(TARGET target) {
ValueContext valueContext = createValueContext();
return converterValidatorChain.convertToPresentation(target,
valueContext);
}
/**
* Handles the value change triggered by the bound field.
*
* @param event
*/
private void handleFieldValueChange(
ValueChangeEvent event) {
// Don't handle change events when setting initial value
if (valueInit || convertedBack) {
convertedBack = false;
return;
}
if (binder != null) {
// Inform binder of changes; if setBean: writeIfValid
getBinder().handleFieldValueChange(this, event);
// Compare the value with initial value, and remove the binder
// from changed bindings if reverted
removeFromChangedBindingsIfReverted(
getBinder()::removeFromChangedBindings);
getBinder().fireValueChangeEvent(event);
}
}
/**
* Write the field value by invoking the setter function on the given
* bean, if the value passes all registered validators. Write value back
* to the field as well if {@code isConvertBackToPresentation()} is
* true.
*
* @param bean
* the bean to set the property value to, not null
*/
private BindingValidationStatus writeFieldValue(BEAN bean) {
assert bean != null;
Result result = doConversion();
if (!isReadOnly()) {
result.ifOk(value -> {
setter.accept(bean, value);
if (convertBackToPresentation && value != null) {
FIELDVALUE converted = convertToFieldType(value);
if (!Objects.equals(field.getValue(), converted)) {
convertedBack = true;
getField().setValue(converted);
}
}
});
}
return toValidationStatus(result);
}
/**
* Returns the {@code Binder} connected to this {@code Binding}
* instance.
*
* @return the binder
*/
protected Binder getBinder() {
return binder;
}
@Override
public BindingValidationStatusHandler getValidationStatusHandler() {
return statusHandler;
}
@Override
public void read(BEAN bean) {
convertAndSetFieldValue(getter.apply(bean));
}
private void convertAndSetFieldValue(TARGET modelValue) {
FIELDVALUE convertedValue = convertToFieldType(modelValue);
try {
getField().setValue(convertedValue);
initialValue = modelValue;
} catch (RuntimeException e) {
/*
* Add an additional hint to the exception for the typical case
* with a field that doesn't accept null values. The non-null
* empty value is used as a heuristic to determine that the
* field doesn't accept null rather than throwing for some other
* reason.
*/
if (convertedValue == null
&& getField().getEmptyValue() != null) {
throw new IllegalStateException(String.format(
"A field of type %s didn't accept a null value."
+ " If null values are expected, then configure a null representation for the binding.",
field.getClass().getName()), e);
} else {
// Otherwise, let the original exception speak for itself
throw e;
}
}
}
@Override
public void setReadOnly(boolean readOnly) {
if (setter == null && !readOnly) {
throw new IllegalStateException(
"Binding with a null setter has to be read-only");
}
this.readOnly = readOnly;
getField().setReadOnly(readOnly);
}
@Override
public boolean isReadOnly() {
return readOnly;
}
@Override
public ValueProvider getGetter() {
return getter;
}
@Override
public Setter getSetter() {
return setter;
}
@Override
public void setAsRequiredEnabled(boolean asRequiredEnabled) {
if (!asRequiredSet) {
throw new IllegalStateException(
"Unable to toggle asRequired validation since "
+ "asRequired has not been set.");
}
if (asRequiredEnabled != isAsRequiredEnabled()) {
field.setRequiredIndicatorVisible(asRequiredEnabled);
}
}
@Override
public boolean isAsRequiredEnabled() {
return field.isRequiredIndicatorVisible();
}
@Override
public void setValidatorsDisabled(boolean validatorsDisabled) {
this.validatorsDisabled = validatorsDisabled;
}
@Override
public boolean isValidatorsDisabled() {
return validatorsDisabled;
}
@Override
public void setConvertBackToPresentation(
boolean convertBackToPresentation) {
this.convertBackToPresentation = convertBackToPresentation;
}
@Override
public boolean isConvertBackToPresentation() {
return convertBackToPresentation;
}
@Override
public boolean hasChanges() throws IllegalStateException {
if (binder == null) {
throw new IllegalStateException(
"This Binding is no longer attached to a Binder");
}
return binder.hasChanges(this);
}
@Override
public SerializableBiPredicate getEqualityPredicate() {
return equalityPredicate;
}
/**
* Compares the new value of the field with its initial value, and
* removes the current binding from the {@code changeBindings}, but only
* if {@code equalityPredicate} is set, or
* {@link #isChangeDetectionEnabled()} returns true.
*
* @param removeBindingAction
* the binding consumer that removes the binding from the
* {@code changeBindings}
*/
private void removeFromChangedBindingsIfReverted(
SerializableConsumer> removeBindingAction) {
if (binder.isChangeDetectionEnabled()
|| equalityPredicate != null) {
doConversion().ifOk(convertedValue -> {
SerializableBiPredicate effectivePredicate = equalityPredicate == null
? Objects::equals
: equalityPredicate;
if (effectivePredicate.test(initialValue, convertedValue)) {
removeBindingAction.accept(this);
}
});
}
}
}
/**
* 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 final 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, ValueContext context) {
ValidationResult validationResult = validator.apply(value, context);
return new ValidationResultWrap<>(value, validationResult);
}
@Override
public T convertToPresentation(T value, ValueContext context) {
return value;
}
}
/**
* Converter decorator-strategy pattern to use initially provided "delegate"
* converter to execute its logic until the {@code setIdentity()} method is
* called. Once the method is called the class changes its behavior to the
* same as {@link Converter#identity()} behavior.
*/
private static class ConverterDelegate
implements Converter {
private Converter delegate;
private ConverterDelegate(Converter converter) {
delegate = converter;
}
@Override
public Result convertToModel(FIELDVALUE value,
ValueContext context) {
if (delegate == null) {
return Result.ok(value);
} else {
return delegate.convertToModel(value, context);
}
}
@Override
public FIELDVALUE convertToPresentation(FIELDVALUE value,
ValueContext context) {
if (delegate == null) {
return value;
} else {
return delegate.convertToPresentation(value, context);
}
}
void setIdentity() {
delegate = null;
}
}
private final PropertySet propertySet;
/**
* Property names that have been used for creating a binding.
*/
private final Map> boundProperties = new HashMap<>();
private final Map, BindingBuilder> incompleteMemberFieldBindings = new IdentityHashMap<>();
private BEAN bean;
private final Collection> bindings = new ArrayList<>();
private final Map, BindingBuilder> incompleteBindings = new IdentityHashMap<>();
private final List> validators = new ArrayList<>();
private final Map, ConverterDelegate>> initialConverters = new IdentityHashMap<>();
private EventRouter eventRouter;
private Label statusLabel;
private BinderValidationStatusHandler statusHandler;
private Set> changedBindings = new LinkedHashSet<>();
private boolean validatorsDisabled = false;
private boolean changeDetectionEnabled = false;
/**
* Creates a binder using a custom {@link PropertySet} implementation for
* finding and resolving property names for
* {@link #bindInstanceFields(Object)}, {@link #bind(HasValue, String)} and
* {@link BindingBuilder#bind(String)}.
*
* @param propertySet
* the property set implementation to use, not null
.
*/
protected Binder(PropertySet propertySet) {
Objects.requireNonNull(propertySet, "propertySet cannot be null");
this.propertySet = propertySet;
}
/**
* Informs the Binder that a value in Binding was changed.
*
* If {@link #readBean(Object)} was used, this method will only validate the
* changed binding and ignore state of other bindings.
*
* If {@link #setBean(Object)} was used, all pending changed bindings will
* be validated and non-changed ones will be ignored. The changed value will
* be written to the bean immediately, assuming that Binder-level validators
* also pass.
*
* @param binding
* the binding whose value has been changed
* @param event
* the value change event
* @since 8.2
*/
protected void handleFieldValueChange(Binding binding,
ValueChangeEvent> event) {
changedBindings.add(binding);
if (getBean() == null) {
binding.validate();
} else {
doWriteIfValid(getBean(), changedBindings);
}
}
/**
* Creates a new binder that uses reflection based on the provided bean type
* to resolve bean properties.
*
* @param beanType
* the bean type to use, not null
*/
public Binder(Class beanType) {
this(BeanPropertySet.get(beanType));
}
/**
* Creates a new binder that uses reflection based on the provided bean type
* to resolve bean properties.
*
* @param beanType
* the bean type to use, not {@code null}
* @param scanNestedDefinitions
* if {@code true}, scan for nested property definitions as well
* @since 8.2
*/
public Binder(Class beanType, boolean scanNestedDefinitions) {
this(BeanPropertySet.get(beanType, scanNestedDefinitions,
PropertyFilterDefinition.getDefaultFilter()));
}
/**
* Creates a new binder without support for creating bindings based on
* property names. Use an alternative constructor, such as
* {@link Binder#Binder(Class)}, to create a binder that support creating
* bindings based on instance fields through
* {@link #bindInstanceFields(Object)}, or based on a property name through
* {@link #bind(HasValue, String)} or {@link BindingBuilder#bind(String)}.
*/
public Binder() {
this(new PropertySet() {
@Override
public Stream> getProperties() {
throw new IllegalStateException(
"This Binder instance was created using the default constructor. "
+ "To be able to use property names and bind to instance fields, create the binder using the Binder(Class beanType) constructor instead.");
}
@Override
public Optional> getProperty(
String name) {
throw new IllegalStateException(
"This Binder instance was created using the default constructor. "
+ "To be able to use property names and bind to instance fields, create the binder using the Binder(Class beanType) constructor instead.");
}
});
}
/**
* Creates a binder using a custom {@link PropertySet} implementation for
* finding and resolving property names for
* {@link #bindInstanceFields(Object)}, {@link #bind(HasValue, String)} and
* {@link BindingBuilder#bind(String)}.
*
* This functionality is provided as static method instead of as a public
* constructor in order to make it possible to use a custom property set
* without creating a subclass while still leaving the public constructors
* focused on the common use cases.
*
* @see Binder#Binder()
* @see Binder#Binder(Class)
*
* @param propertySet
* the property set implementation to use, not null
.
* @param
* the bean type
* @return a new binder using the provided property set, not
* null
*/
public static Binder withPropertySet(
PropertySet propertySet) {
return new Binder<>(propertySet);
}
/**
* Returns the bean that has been bound with {@link #bind}, or null if a
* bean is not currently bound.
*
* @return the currently bound bean if any
*/
public BEAN getBean() {
return bean;
}
/**
* Creates a new binding for the given field. The returned builder may be
* further configured before invoking
* {@link BindingBuilder#bind(ValueProvider, Setter)} which completes the
* binding. Until {@code Binding.bind} is called, the binding has no effect.
*
* Note: 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 BindingBuilder#withNullRepresentation(Object)}.
*
* @param
* the value type of the field
* @param field
* the field to be bound, not null
* @return the new binding
*
* @see #bind(HasValue, ValueProvider, Setter)
*/
public BindingBuilder 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, createNullRepresentationAdapter(field),
this::handleValidationStatus)
.withValidator(field.getDefaultValidator());
}
/**
* Creates a new binding for the given field. The returned builder may be
* further configured before invoking {@link #bindInstanceFields(Object)}.
* Unlike with the {@link #forField(HasValue)} method, no explicit call to
* {@link BindingBuilder#bind(String)} is needed to complete this binding in
* the case that the name of the field matches a field name found in the
* bean.
*
* @param
* the value type of the field
* @param field
* the field to be bound, not null
* @return the new binding builder
*
* @see #forField(HasValue)
* @see #bindInstanceFields(Object)
*/
public BindingBuilder forMemberField(
HasValue field) {
incompleteMemberFieldBindings.put(field, null);
return forField(field);
}
/**
* 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.
*
* Use the {@link #forField(HasValue)} overload instead if you want to
* further configure the new binding.
*
* Note: 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 BindingBuilder#withNullRepresentation(Object)}.
*
* When a bean is bound with {@link Binder#setBean(Object)}, 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 bean, 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.bind(nameField, Person::getName, Person::setName);
*
*
*
* Note: when a {@code null} setter is given the field will
* be marked as read-only by invoking {@link HasValue#setReadOnly(boolean)}.
*
* @param
* the value type of the field
* @param field
* the field to bind, not null
* @param getter
* the function to get the value of the property to the field,
* not null
* @param setter
* the function to write the field value to the property or null
* if read-only
* @return the newly created binding
*/
public Binding bind(
HasValue field, ValueProvider getter,
Setter setter) {
return forField(field).bind(getter, setter);
}
/**
* Binds the given field to the property with the given name. The getter and
* setter of the property are looked up using a {@link PropertySet}.
*
* For a Binder
created using the {@link Binder#Binder(Class)}
* constructor, introspection will be used to find a Java Bean property. If
* a JSR-303 bean validation implementation is present on the classpath, a
* {@link BeanValidator} is also added to the binding.
*
* The property must have an accessible getter method. It need not have an
* accessible setter; in that case the property value is never updated and
* the binding is said to be read-only.
*
* @param
* the value type of the field to bind
* @param field
* the field to bind, not null
* @param propertyName
* the name of the property to bind, not null
* @return the newly created binding
*
* @throws IllegalArgumentException
* if the property name is invalid
* @throws IllegalArgumentException
* if the property has no accessible getter
* @throws IllegalStateException
* if the binder is not configured with an appropriate
* {@link PropertySet}
*
* @see #bind(HasValue, ValueProvider, Setter)
*/
public Binding bind(
HasValue field, String propertyName) {
return forField(field).bind(propertyName);
}
/**
* Binds the given bean to all the fields added to this Binder. A
* {@code null} value removes a currently bound bean.
*
* When a bean is bound, the field values are updated by invoking their
* corresponding getter functions. Any changes to field values are reflected
* back to their corresponding property values of the bean as long as the
* bean is bound.
*
* Note: Any change made in one of the bound fields runs validation for only
* the changed {@link Binding}, and additionally any bean level validation
* for this binder (bean level validators are added using
* {@link Binder#withValidator(Validator)}. As a result, the bean set via
* this method is not guaranteed to always be in a valid state. This means
* also that possible {@link StatusChangeListener} and
* {@link BinderValidationStatusHandler} are called indicating a successful
* validation, even though some bindings can be in a state that would not
* pass validation. If bean validity is required at all times,
* {@link #readBean(Object)} and {@link #writeBean(Object)} should be used
* instead.
*
* After updating each field, the value is read back from the field and the
* bean's property value is updated if it has been changed from the original
* value by the field or a converter.
*
* @see #readBean(Object)
* @see #writeBean(Object)
* @see #writeBeanIfValid(Object)
*
* @param bean
* the bean to edit, or {@code null} to remove a currently bound
* bean and clear bound fields
*/
public void setBean(BEAN bean) {
checkBindingsCompleted("setBean");
if (bean == null) {
if (this.bean != null) {
doRemoveBean(true);
clearFields();
}
} else {
doRemoveBean(false);
this.bean = bean;
getBindings().forEach(b -> b.initFieldValue(bean, true));
// if there has been field value change listeners that trigger
// validation, need to make sure the validation errors are cleared
getValidationStatusHandler().statusChange(
BinderValidationStatus.createUnresolvedStatus(this));
fireStatusChangeEvent(false);
}
}
/**
* Removes the currently set bean and clears bound fields. If there is no
* bound bean, does nothing.
*
* This is a shorthand for {@link #setBean(Object)} with {@code null} bean.
*/
public void removeBean() {
setBean(null);
}
/**
* Reads the bound property values from the given bean to the corresponding
* fields.
*
* The bean is not otherwise associated with this binder; in particular its
* property values are not bound to the field value changes. To achieve
* that, use {@link #setBean(Object)}.
*
* @see #setBean(Object)
* @see #writeBeanIfValid(Object)
* @see #writeBean(Object)
*
* @param bean
* the bean whose property values to read or {@code null} to
* clear bound fields
*/
public void readBean(BEAN bean) {
checkBindingsCompleted("readBean");
if (bean == null) {
clearFields();
} else {
getBindings().forEach(binding -> {
// Some bindings may have been removed from binder
// during readBean. We should skip those bindings to
// avoid NPE inside initFieldValue. It happens e.g. when
// we unbind a binding in valueChangeListener of another
// field.
if (binding.getField() != null) {
binding.initFieldValue(bean, false);
}
});
changedBindings.clear();
getValidationStatusHandler().statusChange(
BinderValidationStatus.createUnresolvedStatus(this));
fireStatusChangeEvent(false);
}
}
/**
* Refreshes the fields values by reading them again from the currently
* associated bean via invoking their corresponding value provider methods.
*
* If no bean is currently associated with this binder
* ({@link #setBean(Object)} has not been called before invoking this
* method), the bound fields will be cleared.
*
*
* @see #setBean(Object)
* @see #readBean(Object)
* @see #writeBean(Object)
* @see #writeBeanIfValid(Object)
*/
public void refreshFields() {
readBean(bean);
}
/**
* Writes changes from the bound fields to the given bean if all validators
* (binding and bean level) pass.
*
* If any field binding validator fails, no values are written and a
* {@code ValidationException} is thrown.
*
*
* If all field level validators pass, the given bean is updated and bean
* level validators are run on the updated bean. If any bean level validator
* fails, the bean updates are reverted and a {@code ValidationException} is
* thrown.
*
*
* @see #writeBeanIfValid(Object)
* @see #readBean(Object)
* @see #setBean(Object)
*
* @param bean
* the object to which to write the field values, not
* {@code null}
* @throws ValidationException
* if some of the bound field values fail to validate
*/
public void writeBean(BEAN bean) throws ValidationException {
writeBean(bean, bindings);
}
/**
* Writes changes from the given bindings to the given bean if all
* validators (binding and bean level) pass.
*
* If any field binding validator fails, no values are written and a
* {@code ValidationException} is thrown.
*
* If all field level validators pass, the given bean is updated and bean
* level validators are run on the updated bean. If any bean level validator
* fails, the bean updates are reverted and a {@code ValidationException} is
* thrown.
*
* @see #writeBeanIfValid(Object)
* @see #writeBean(Object)
* @see #readBean(Object)
* @see #setBean(Object)
* @see #writeChangedBindingsToBean(Object)
*
* @param bean
* the object to which to write the field values, not
* {@code null}
* @param bindingsToWrite
* Collection of bindings to use in writing the bean
* @throws ValidationException
* if some of the bound field values fail to validate
* @throws IllegalArgumentException
* if bindingsToWrite contains bindings not belonging to this
* Binder
*
* @since 8.24
*/
public void writeBean(BEAN bean,
Collection> bindingsToWrite)
throws ValidationException {
if (!bindings.containsAll(bindingsToWrite)) {
throw new IllegalArgumentException(
"Can't write bean using binding that is not bound to this Binder.");
}
BinderValidationStatus status = doWriteIfValid(bean,
bindingsToWrite);
if (status.hasErrors()) {
throw new ValidationException(status.getFieldValidationErrors(),
status.getBeanValidationErrors());
}
}
/**
* Writes changes from the changed bindings to the given bean if all
* validators (binding and bean level) pass. If the bean is the same
* instance where Binder read the bean, this method updates the bean with
* the changes.
*
* If any field binding validator fails, no values are written and a
* {@code ValidationException} is thrown.
*
* If all field level validators pass, the given bean is updated and bean
* level validators are run on the updated bean. If any bean level validator
* fails, the bean updates are reverted and a {@code ValidationException} is
* thrown.
*
* @see #writeBeanIfValid(Object)
* @see #writeBean(Object)
* @see #readBean(Object)
* @see #setBean(Object)
*
* @param bean
* the object to which to write the field values, not
* {@code null}
* @throws ValidationException
* if some of the bound field values fail to validate
*
* @since 8.24
*/
public void writeChangedBindingsToBean(BEAN bean)
throws ValidationException {
writeBean(bean, getChangedBindings());
}
/**
* Get the immutable Set of changed bindings.
*
* @see #hasChanges()
*
* @return Immutable set of bindings.
*
* @since 8.24
*/
public Set> getChangedBindings() {
return Collections.unmodifiableSet(changedBindings);
}
/**
* Writes successfully converted and validated changes from the bound fields
* to the bean even if there are other fields with non-validated changes.
*
* @see #writeBean(Object)
* @see #writeBeanIfValid(Object)
* @see #readBean(Object)
* @see #setBean(Object)
*
* @param bean
* the object to which to write the field values, not
* {@code null}
*
* @since 8.10
*/
public void writeBeanAsDraft(BEAN bean) {
doWriteDraft(bean, new ArrayList<>(bindings), false);
}
/**
* Writes successfully converted changes from the bound fields bypassing all
* the Validation, or all fields passing conversion if forced = true. If the
* conversion fails, the value written to the bean will be null.
*
* @see #writeBean(Object)
* @see #writeBeanIfValid(Object)
* @see #readBean(Object)
* @see #setBean(Object)
*
* @param bean
* the object to which to write the field values, not
* {@code null}
* @param forced
* disable all Validators during write
* @since 8.11
*/
public void writeBeanAsDraft(BEAN bean, boolean forced) {
doWriteDraft(bean, new ArrayList<>(bindings), forced);
}
/**
* Writes changes from the bound fields to the given bean if all validators
* (binding and bean level) pass.
*
* If any field binding validator fails, no values are written and
* false
is returned.
*
* If all field level validators pass, the given bean is updated and bean
* level validators are run on the updated bean. If any bean level validator
* fails, the bean updates are reverted and false
is returned.
*
* @see #writeBean(Object)
* @see #readBean(Object)
* @see #setBean(Object)
*
* @param bean
* the object to which to write the field values, not
* {@code null}
* @return {@code true} if there was no validation errors and the bean was
* updated, {@code false} otherwise
*/
public boolean writeBeanIfValid(BEAN bean) {
return doWriteIfValid(bean, bindings).isOk();
}
/**
* Writes the field values into the given bean if all field level validators
* pass. Runs bean level validators on the bean after writing.
*
* Note: The collection of bindings is cleared on
* successful save.
*
* @param bean
* the bean to write field values into
* @param bindings
* the set of bindings to write to the bean
* @return a list of field validation errors if such occur, otherwise a list
* of bean validation errors.
*/
@SuppressWarnings({ "unchecked" })
private BinderValidationStatus doWriteIfValid(BEAN bean,
Collection> bindings) {
Objects.requireNonNull(bean, "bean cannot be null");
List binderResults = Collections.emptyList();
// make a copy of the incoming bindings to avoid their modifications
// during validation
Collection> currentBindings = new ArrayList<>(
bindings);
// First run fields level validation, if no validation errors then
// update bean.
List> bindingResults = currentBindings
.stream().map(b -> b.validate(false))
.collect(Collectors.toList());
if (bindingResults.stream()
.noneMatch(BindingValidationStatus::isError)) {
// Store old bean values so we can restore them if validators fail
Map, Object> oldValues = getBeanState(bean,
currentBindings);
// Field level validation can be skipped as it was done already
boolean validatorsInUse = isValidatorsDisabled();
setValidatorsDisabled(true);
currentBindings
.forEach(binding -> ((BindingImpl) binding)
.writeFieldValue(bean));
setValidatorsDisabled(validatorsInUse);
// Now run bean level validation against the updated bean
binderResults = validateBean(bean);
if (binderResults.stream().anyMatch(ValidationResult::isError)) {
// Bean validator failed, revert values
restoreBeanState(bean, oldValues);
} else if (bean.equals(getBean())) {
/*
* Changes have been successfully saved. The set is only cleared
* when the changes are stored in the currently set bean.
*/
changedBindings.clear();
} else if (getBean() == null) {
/*
* When using readBean and writeBean there is no knowledge of
* which bean the changes come from or are stored in. Binder is
* no longer "changed" when saved succesfully to any bean.
*/
changedBindings.clear();
}
}
// Generate status object and fire events.
BinderValidationStatus status = new BinderValidationStatus<>(this,
bindingResults, binderResults);
getValidationStatusHandler().statusChange(status);
fireStatusChangeEvent(!status.isOk());
return status;
}
/**
* Writes the successfully converted and validated field values into the
* given bean.
*
* @param bean
* the bean to write field values into
* @param bindings
* the set of bindings to write to the bean
* @param forced
* disable validators during write if true
*/
private void doWriteDraft(BEAN bean, Collection> bindings,
boolean forced) {
Objects.requireNonNull(bean, "bean cannot be null");
if (!forced) {
bindings.forEach(binding -> ((BindingImpl) binding)
.writeFieldValue(bean));
} else {
boolean isDisabled = isValidatorsDisabled();
setValidatorsDisabled(true);
bindings.forEach(binding -> ((BindingImpl) binding)
.writeFieldValue(bean));
setValidatorsDisabled(isDisabled);
}
}
/**
* Restores the state of the bean from the given values. This method is used
* together with {@link #getBeanState(Object, Collection)} to provide a way
* to revert changes in case the bean validation fails after save.
*
* @param bean
* the bean
* @param oldValues
* the old values
*
* @since 8.2
*/
@SuppressWarnings({ "unchecked", "rawtypes" })
protected void restoreBeanState(BEAN bean,
Map, Object> oldValues) {
getBindings().stream().filter(oldValues::containsKey)
.forEach(binding -> {
Setter setter = binding.setter;
if (setter != null) {
setter.accept(bean, oldValues.get(binding));
}
});
}
/**
* Stores the state of the given bean. This method is used together with
* {@link #restoreBeanState(Object, Map)} to provide a way to revert changes
* in case the bean validation fails after save.
*
* @param bean
* the bean to store the state of
* @param bindings
* the bindings to store
*
* @return map from binding to value
*
* @since 8.2
*/
@SuppressWarnings({ "unchecked", "rawtypes" })
protected Map, Object> getBeanState(BEAN bean,
Collection> bindings) {
Map, Object> oldValues = new HashMap<>();
bindings.stream().map(binding -> (BindingImpl) binding)
.filter(binding -> binding.setter != null)
.forEach(binding -> oldValues.put(binding,
binding.getter.apply(bean)));
return oldValues;
}
/**
* Adds an bean level validator.
*
* Bean level validators are applied on the bean instance after the bean is
* updated. If the validators fail, the bean instance is reverted to its
* previous state.
*
* @see #writeBean(Object)
* @see #writeBeanIfValid(Object)
* @see #withValidator(SerializablePredicate, String)
* @see #withValidator(SerializablePredicate, ErrorMessageProvider)
*
* @param validator
* the validator to add, not null
* @return this binder, for chaining
*/
public Binder withValidator(Validator super BEAN> validator) {
Objects.requireNonNull(validator, "validator cannot be null");
Validator super BEAN> wrappedValidator = ((value, context) -> {
if (isValidatorsDisabled()) {
return ValidationResult.ok();
} else {
return validator.apply(value, context);
}
});
validators.add(wrappedValidator);
return this;
}
/**
* A convenience method to add a validator to this binder using the
* {@link Validator#from(SerializablePredicate, String)} factory method.
*
* Bean level validators are applied on the bean instance after the bean is
* updated. If the validators fail, the bean instance is reverted to its
* previous state.
*
* @see #writeBean(Object)
* @see #writeBeanIfValid(Object)
* @see #withValidator(Validator)
* @see #withValidator(SerializablePredicate, ErrorMessageProvider)
*
* @param predicate
* the predicate performing validation, not null
* @param message
* the error message to report in case validation failure
* @return this binder, for chaining
*/
public Binder withValidator(SerializablePredicate predicate,
String message) {
return withValidator(Validator.from(predicate, message));
}
/**
* A convenience method to add a validator to this binder using the
* {@link Validator#from(SerializablePredicate, ErrorMessageProvider)}
* factory method.
*
* Bean level validators are applied on the bean instance after the bean is
* updated. If the validators fail, the bean instance is reverted to its
* previous state.
*
* @see #writeBean(Object)
* @see #writeBeanIfValid(Object)
* @see #withValidator(Validator)
* @see #withValidator(SerializablePredicate, String)
*
* @param predicate
* the predicate performing validation, not null
* @param errorMessageProvider
* the provider to generate error messages, not null
* @return this binder, for chaining
*/
public Binder withValidator(SerializablePredicate predicate,
ErrorMessageProvider errorMessageProvider) {
return withValidator(Validator.from(predicate, errorMessageProvider));
}
/**
* Clear all the bound fields for this binder.
*/
private void clearFields() {
bindings.forEach(binding -> {
binding.getField().clear();
clearError(binding.getField());
});
if (hasChanges()) {
fireStatusChangeEvent(false);
}
changedBindings.clear();
}
/**
* Validates the values of all bound fields and returns the validation
* status.
*
* If all field level validators pass, and {@link #setBean(Object)} has been
* used to bind to a bean, bean level validators are run for that bean. Bean
* level validators are ignored if there is no bound bean or if any field
* level validator fails.
*
* Note: This method will attempt to temporarily apply all
* current changes to the bean and run full bean validation for it. The
* changes are reverted after bean validation.
*
* @return validation status for the binder
*/
public BinderValidationStatus validate() {
return validate(true);
}
/**
* Validates the values of all bound fields and returns the validation
* status. This method can fire validation status events. Firing the events
* depends on the given {@code boolean}.
*
* @param fireEvent
* {@code true} to fire validation status events; {@code false}
* to not
* @return validation status for the binder
*
* @since 8.2
*/
protected BinderValidationStatus validate(boolean fireEvent) {
if (getBean() == null && !validators.isEmpty()) {
throw new IllegalStateException("Cannot validate binder: "
+ "bean level validators have been configured "
+ "but no bean is currently set");
}
List> bindingStatuses = validateBindings();
BinderValidationStatus validationStatus;
if (validators.isEmpty() || bindingStatuses.stream()
.anyMatch(BindingValidationStatus::isError)) {
validationStatus = new BinderValidationStatus<>(this,
bindingStatuses, Collections.emptyList());
} else {
Map, Object> beanState = getBeanState(getBean(),
changedBindings);
changedBindings
.forEach(binding -> ((BindingImpl) binding)
.writeFieldValue(getBean()));
validationStatus = new BinderValidationStatus<>(this,
bindingStatuses, validateBean(getBean()));
restoreBeanState(getBean(), beanState);
}
if (fireEvent) {
getValidationStatusHandler().statusChange(validationStatus);
fireStatusChangeEvent(validationStatus.hasErrors());
}
return validationStatus;
}
/**
* Runs all currently configured field level validators, as well as all bean
* level validators if a bean is currently set with
* {@link #setBean(Object)}, and returns whether any of the validators
* failed.
*
* Note: Calling this method will not trigger status change events,
* unlike {@link #validate()} and will not modify the UI. To also update
* error indicators on fields, use {@code validate().isOk()}.
*
* Note: This method will attempt to temporarily apply all
* current changes to the bean and run full bean validation for it. The
* changes are reverted after bean validation.
*
* @see #validate()
*
* @return whether this binder is in a valid state
* @throws IllegalStateException
* if bean level validators have been configured and no bean is
* currently set
*/
public boolean isValid() {
return validate(false).isOk();
}
/**
* Validates the bindings and returns the result of the validation as a list
* of validation statuses.
*
* Does not run bean validators.
*
* @see #validateBean(Object)
*
* @return an immutable list of validation results for bindings
*/
private List> validateBindings() {
return getBindings().stream().map(BindingImpl::doValidation)
.collect(Collectors.toList());
}
/**
* Validates the {@code bean} using validators added using
* {@link #withValidator(Validator)} and returns the result of the
* validation as a list of validation results.
*
*
* @see #withValidator(Validator)
*
* @param bean
* the bean to validate
* @return a list of validation errors or an empty list if validation
* succeeded
*/
private List validateBean(BEAN bean) {
Objects.requireNonNull(bean, "bean cannot be null");
List results = Collections.unmodifiableList(validators
.stream()
.map(validator -> validator.apply(bean, new ValueContext()))
.collect(Collectors.toList()));
return results;
}
/**
* Sets the label to show the binder level validation errors not related to
* any specific field.
*
* Only the one validation error message is shown in this label at a time.
*
* This is a convenience method for
* {@link #setValidationStatusHandler(BinderValidationStatusHandler)}, 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 #setValidationStatusHandler(BinderValidationStatusHandler)
* @see BindingBuilder#withStatusLabel(Label)
*/
public void setStatusLabel(Label statusLabel) {
if (statusHandler != null) {
throw new IllegalStateException("Cannot set status label if a "
+ BinderValidationStatusHandler.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