/* * 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.lang.reflect.Field; import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Type; import java.util.ArrayList; import java.util.Arrays; 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.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.StringToIntegerConverter; import com.vaadin.data.validator.BeanValidator; import com.vaadin.event.EventRouter; import com.vaadin.server.ErrorMessage; 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.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. * <p> * A binder is a collection of <i>bindings</i>, each representing the mapping of * a single field, through converters and validators, to a backing property. * <p> * 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 <i>master-details</i> * view, where a select component is used to pick the bean to edit. * <p> * 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. * <p> * 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. * <p> * Unless otherwise specified, {@code Binder} method arguments cannot be null. * * @author Vaadin Ltd. * * @param <BEAN> * the bean type * * @see BindingBuilder * @see Binding * @see HasValue * * @since 8.0 */ public class Binder<BEAN> implements Serializable { /** * Represents the binding between a field and a data property. * * @param <BEAN> * the bean type * @param <TARGET> * the target data type of the binding, matches the field type * unless a converter has been set * * @see Binder#forField(HasValue) */ public interface Binding<BEAN, TARGET> 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. * * @see Binder#validate() * @see Validator#apply(Object, ValueContext) * * @return the validation result. */ public BindingValidationStatus<TARGET> validate(); } /** * Creates a binding between a field and a data property. * * @param <BEAN> * the bean type * @param <TARGET> * the target data type of the binding, matches the field type * until a converter has been set * * @see Binder#forField(HasValue) */ public interface BindingBuilder<BEAN, TARGET> 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. * <p> * When a bean is bound with {@link Binder#setBean(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 <i>read-only</i>. * <p> * If the Binder is already bound to some bean, the newly bound field is * associated with the corresponding bean property as described above. * <p> * 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: * * <pre> * class Person { * public String getName() { ... } * public void setName(String name) { ... } * } * * TextField nameField = new TextField(); * binder.forField(nameField).bind(Person::getName, Person::setName); * </pre> * * @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<BEAN, TARGET> bind(ValueProvider<BEAN, TARGET> getter, Setter<BEAN, TARGET> 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}. * <p> * For a <code>Binder</code> 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. * <p> * 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 <i>read-only</i>. * * @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<BEAN, TARGET> 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<BEAN, TARGET> withValidator( Validator<? super TARGET> validator); /** * A convenience method to add a validator to this binding using the * {@link Validator#from(SerializablePredicate, String)} factory method. * <p> * 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, 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<BEAN, TARGET> 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, ErrorMessageProvider)} * factory method. * <p> * 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 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<BEAN, TARGET> withValidator( SerializablePredicate<? super TARGET> predicate, ErrorMessageProvider errorMessageProvider) { return withValidator( Validator.from(predicate, errorMessageProvider)); } /** * Maps the binding to another data type using the given * {@link Converter}. * <p> * 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. * <p> * For instance, a {@code TextField} can be bound to an integer-typed * property using an appropriate converter such as a * {@link StringToIntegerConverter}. * * @param <NEWTARGET> * 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 <NEWTARGET> BindingBuilder<BEAN, NEWTARGET> withConverter( Converter<TARGET, NEWTARGET> converter); /** * Maps the binding to another data type using the mapping functions and * a possible exception as the error message. * <p> * 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. * <p> * For instance, a {@code TextField} can be bound to an integer-typed * property using appropriate functions such as: * <code>withConverter(Integer::valueOf, String::valueOf);</code> * * @param <NEWTARGET> * 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 <NEWTARGET> BindingBuilder<BEAN, NEWTARGET> withConverter( SerializableFunction<TARGET, NEWTARGET> toModel, SerializableFunction<NEWTARGET, TARGET> 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. * <p> * 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. * <p> * For instance, a {@code TextField} can be bound to an integer-typed * property using appropriate functions such as: * <code>withConverter(Integer::valueOf, String::valueOf);</code> * * @param <NEWTARGET> * 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 * <code>toModel</code> fails * @return a new binding with the appropriate type * @throws IllegalStateException * if {@code bind} has already been called */ public default <NEWTARGET> BindingBuilder<BEAN, NEWTARGET> withConverter( SerializableFunction<TARGET, NEWTARGET> toModel, SerializableFunction<NEWTARGET, TARGET> 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<BEAN, TARGET> 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. * <p> * 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. * <p> * 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). * <p> * 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<BEAN, TARGET> 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. * <p> * 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. * <p> * 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). * <p> * 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<BEAN, TARGET> withValidationStatusHandler( BindingValidationStatusHandler handler); /** * Sets the field to be required. This means two things: * <ol> * <li>the required indicator is visible</li> * <li>the field value is validated for not being empty*</li> * </ol> * For localizing the error message, use * {@link #asRequired(ErrorMessageProvider)}. * <p> * *Value not being the equal to what {@link HasValue#getEmptyValue()} * returns. * * @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<BEAN, TARGET> asRequired( String errorMessage) { return asRequired(context -> errorMessage); } /** * Sets the field to be required. This means two things: * <ol> * <li>the required indicator is visible</li> * <li>the field value is validated for not being empty*</li> * </ol> * *Value not being the 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<BEAN, TARGET> asRequired( ErrorMessageProvider errorMessageProvider); } /** * An internal implementation of {@code BindingBuilder}. * * @param <BEAN> * the bean type, must match the Binder bean type * @param <FIELDVALUE> * the value type of the field * @param <TARGET> * the target data type of the binding, matches the field type * until a converter has been set */ protected static class BindingBuilderImpl<BEAN, FIELDVALUE, TARGET> implements BindingBuilder<BEAN, TARGET> { private final Binder<BEAN> binder; private final HasValue<FIELDVALUE> field; private BindingValidationStatusHandler statusHandler; private boolean isStatusHandlerChanged; private boolean bound; /** * Contains all converters and validators chained together in the * correct order. */ private Converter<FIELDVALUE, TARGET> converterValidatorChain; /** * 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<BEAN> binder, HasValue<FIELDVALUE> field, Converter<FIELDVALUE, TARGET> converterValidatorChain, BindingValidationStatusHandler statusHandler) { this.field = field; this.binder = binder; this.converterValidatorChain = converterValidatorChain; this.statusHandler = statusHandler; } @Override public Binding<BEAN, TARGET> bind(ValueProvider<BEAN, TARGET> getter, Setter<BEAN, TARGET> setter) { checkUnbound(); Objects.requireNonNull(getter, "getter cannot be null"); BindingImpl<BEAN, FIELDVALUE, TARGET> binding = new BindingImpl<>( this, getter, setter); getBinder().bindings.add(binding); if (getBinder().getBean() != null) { binding.initFieldValue(getBinder().getBean()); } getBinder().fireStatusChangeEvent(false); bound = true; getBinder().incompleteBindings.remove(getField()); return binding; } @Override @SuppressWarnings({ "unchecked", "rawtypes" }) public Binding<BEAN, TARGET> bind(String propertyName) { Objects.requireNonNull(propertyName, "Property name cannot be null"); checkUnbound(); PropertyDefinition<BEAN, ?> definition = getBinder().propertySet .getProperty(propertyName) .orElseThrow(() -> new IllegalArgumentException( "Could not resolve property name " + propertyName + " from " + getBinder().propertySet)); ValueProvider<BEAN, ?> getter = definition.getGetter(); Setter<BEAN, ?> setter = definition.getSetter() .orElse((bean, value) -> { // Setter ignores value }); BindingBuilder<BEAN, ?> finalBinding = withConverter( createConverter(definition.getType()), false); finalBinding = getBinder().configureBinding(finalBinding, definition); try { Binding binding = ((BindingBuilder) finalBinding).bind(getter, setter); getBinder().boundProperties.put(propertyName, binding); return binding; } finally { getBinder().incompleteMemberFieldBindings.remove(getField()); } } @SuppressWarnings("unchecked") private Converter<TARGET, Object> createConverter(Class<?> getterType) { return Converter.from(fieldValue -> getterType.cast(fieldValue), propertyValue -> (TARGET) propertyValue, exception -> { throw new RuntimeException(exception); }); } @Override public BindingBuilder<BEAN, TARGET> withValidator( Validator<? super TARGET> validator) { checkUnbound(); Objects.requireNonNull(validator, "validator cannot be null"); converterValidatorChain = converterValidatorChain .chain(new ValidatorAsConverter<>(validator)); return this; } @Override public <NEWTARGET> BindingBuilder<BEAN, NEWTARGET> withConverter( Converter<TARGET, NEWTARGET> converter) { return withConverter(converter, true); } @Override public BindingBuilder<BEAN, TARGET> 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<BEAN, TARGET> asRequired( ErrorMessageProvider errorMessageProvider) { checkUnbound(); field.setRequiredIndicatorVisible(true); return withValidator( value -> !Objects.equals(value, field.getEmptyValue()), errorMessageProvider); } /** * Implements {@link #withConverter(Converter)} method with additional * possibility to disable (reset) default null representation converter. * <p> * 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 <NEWTARGET> * the type to convert to * @throws IllegalStateException * if {@code bind} has already been called */ protected <NEWTARGET> BindingBuilder<BEAN, NEWTARGET> withConverter( Converter<TARGET, NEWTARGET> converter, boolean resetNullRepresentation) { checkUnbound(); Objects.requireNonNull(converter, "converter cannot be null"); if (resetNullRepresentation) { getBinder().initialConverters.get(field).setIdentity(); } return getBinder().createBinding(field, converterValidatorChain.chain(converter), statusHandler); } /** * Returns the {@code Binder} connected to this {@code Binding} * instance. * * @return the binder */ protected Binder<BEAN> 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<FIELDVALUE> getField() { return field; } } /** * An internal implementation of {@code Binding}. * * @param <BEAN> * the bean type, must match the Binder bean type * @param <FIELDVALUE> * the value type of the field * @param <TARGET> * the target data type of the binding, matches the field type * unless a converter has been set */ protected static class BindingImpl<BEAN, FIELDVALUE, TARGET> implements Binding<BEAN, TARGET> { private final Binder<BEAN> binder; private final HasValue<FIELDVALUE> field; private final BindingValidationStatusHandler statusHandler; private final SerializableFunction<BEAN, TARGET> getter; private final Setter<BEAN, TARGET> setter; // Not final since we temporarily remove listener while changing values private Registration onValueChange; /** * Contains all converters and validators chained together in the * correct order. */ private final Converter<FIELDVALUE, TARGET> converterValidatorChain; public BindingImpl(BindingBuilderImpl<BEAN, FIELDVALUE, TARGET> builder, SerializableFunction<BEAN, TARGET> getter, Setter<BEAN, TARGET> setter) { this.binder = builder.getBinder(); this.field = builder.field; this.statusHandler = builder.statusHandler; converterValidatorChain = builder.converterValidatorChain; onValueChange = getField() .addValueChangeListener(this::handleFieldValueChange); this.getter = getter; this.setter = setter; } @Override public HasValue<FIELDVALUE> 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<TARGET> validate() { BindingValidationStatus<TARGET> status = doValidation(); getBinder().getValidationStatusHandler() .statusChange(new BinderValidationStatus<>(getBinder(), Arrays.asList(status), Collections.emptyList())); getBinder().fireStatusChangeEvent(status.isError()); return status; } /** * 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<TARGET> doConversion() { FIELDVALUE fieldValue = field.getValue(); return converterValidatorChain.convertToModel(fieldValue, createValueContext()); } private BindingValidationStatus<TARGET> toValidationStatus( Result<TARGET> result) { return new BindingValidationStatus<>(this, result.isError() ? ValidationResult.error(result.getMessage().get()) : ValidationResult.ok()); } /** * 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<TARGET> 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); } return new ValueContext(findLocale()); } /** * Sets the field value by invoking the getter function on the given * bean. The default listener attached to the field will be removed for * the duration of this update. * * @param bean * the bean to fetch the property value from */ private void initFieldValue(BEAN bean) { assert bean != null; assert onValueChange != null; onValueChange.remove(); try { getField().setValue(convertDataToFieldType(bean)); } finally { onValueChange = getField() .addValueChangeListener(this::handleFieldValueChange); } } private FIELDVALUE convertDataToFieldType(BEAN bean) { return converterValidatorChain.convertToPresentation( getter.apply(bean), createValueContext()); } /** * Handles the value change triggered by the bound field. * * @param event */ private void handleFieldValueChange( ValueChangeEvent<FIELDVALUE> event) { getBinder().setHasChanges(true); List<ValidationResult> binderValidationResults = Collections .emptyList(); BindingValidationStatus<TARGET> fieldValidationStatus; if (getBinder().getBean() != null) { BEAN bean = getBinder().getBean(); fieldValidationStatus = writeFieldValue(bean); if (!getBinder().bindings.stream() .map(BindingImpl::doValidation) .anyMatch(BindingValidationStatus::isError)) { binderValidationResults = getBinder().validateBean(bean); if (!binderValidationResults.stream() .anyMatch(ValidationResult::isError)) { getBinder().setHasChanges(false); } } } else { fieldValidationStatus = doValidation(); } BinderValidationStatus<BEAN> status = new BinderValidationStatus<>( getBinder(), Arrays.asList(fieldValidationStatus), binderValidationResults); getBinder().getValidationStatusHandler().statusChange(status); getBinder().fireStatusChangeEvent(status.hasErrors()); getBinder().fireValueChangeEvent(event); } /** * Write 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 BindingValidationStatus<TARGET> writeFieldValue(BEAN bean) { assert bean != null; Result<TARGET> result = doConversion(); if (setter != null) { result.ifOk(value -> setter.accept(bean, value)); } return toValidationStatus(result); } /** * Returns the {@code Binder} connected to this {@code Binding} * instance. * * @return the binder */ protected Binder<BEAN> getBinder() { return binder; } private void notifyStatusHandler(BindingValidationStatus<?> status) { statusHandler.statusChange(status); } } /** * Wraps a validator as a converter. * <p> * The type of the validator must be of the same type as this converter or a * super type of it. * * @param <T> * the type of the converter */ private static class ValidatorAsConverter<T> implements Converter<T, T> { 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<T> convertToModel(T value, ValueContext context) { ValidationResult validationResult = validator.apply(value, context); if (validationResult.isError()) { return Result.error(validationResult.getErrorMessage()); } else { return Result.ok(value); } } @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<FIELDVALUE> implements Converter<FIELDVALUE, FIELDVALUE> { private Converter<FIELDVALUE, FIELDVALUE> delegate; private ConverterDelegate(Converter<FIELDVALUE, FIELDVALUE> converter) { delegate = converter; } @Override public Result<FIELDVALUE> 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<BEAN> propertySet; /** * Property names that have been used for creating a binding. */ private final Map<String, Binding<BEAN, ?>> boundProperties = new HashMap<>(); private final Map<HasValue<?>, BindingBuilder<BEAN, ?>> incompleteMemberFieldBindings = new IdentityHashMap<>(); private BEAN bean; private final Set<BindingImpl<BEAN, ?, ?>> bindings = new LinkedHashSet<>(); private final Map<HasValue<?>, BindingBuilder<BEAN, ?>> incompleteBindings = new IdentityHashMap<>(); private final List<Validator<? super BEAN>> validators = new ArrayList<>(); private final Map<HasValue<?>, ConverterDelegate<?>> initialConverters = new IdentityHashMap<>(); private EventRouter eventRouter; private Label statusLabel; private BinderValidationStatusHandler<BEAN> statusHandler; private boolean hasChanges = 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 <code>null</code>. */ protected Binder(PropertySet<BEAN> propertySet) { Objects.requireNonNull(propertySet, "propertySet cannot be null"); this.propertySet = propertySet; } /** * 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</code> */ public Binder(Class<BEAN> beanType) { this(BeanPropertySet.get(beanType)); } /** * 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<BEAN>() { @Override public Stream<PropertyDefinition<BEAN, ?>> 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<BEAN> beanType) constructor instead."); } @Override public Optional<PropertyDefinition<BEAN, ?>> 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<BEAN> 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)}. * <p> * 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 <code>null</code>. * @return a new binder using the provided property set, not * <code>null</code> */ public static <BEAN> Binder<BEAN> withPropertySet( PropertySet<BEAN> 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. * <p> * <strong>Note:</strong> Not all {@link HasValue} implementations support * passing {@code null} as the value. For these the Binder will * automatically change {@code null} to a null representation provided by * {@link HasValue#getEmptyValue()}. This conversion is one-way only, if you * want to have a two-way mapping back to {@code null}, use * {@link BindingBuilder#withNullRepresentation(Object)}. * * @param <FIELDVALUE> * 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 <FIELDVALUE> BindingBuilder<BEAN, FIELDVALUE> forField( HasValue<FIELDVALUE> 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); } /** * 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 <FIELDVALUE> * 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 <FIELDVALUE> BindingBuilder<BEAN, FIELDVALUE> forMemberField( HasValue<FIELDVALUE> 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. * <p> * Use the {@link #forField(HasValue)} overload instead if you want to * further configure the new binding. * <p> * <strong>Note:</strong> Not all {@link HasValue} implementations support * passing {@code null} as the value. For these the Binder will * automatically change {@code null} to a null representation provided by * {@link HasValue#getEmptyValue()}. This conversion is one-way only, if you * want to have a two-way mapping back to {@code null}, use * {@link #forField(HasValue)} and * {@link BindingBuilder#withNullRepresentation(Object)}. * <p> * When a bean is bound with {@link Binder#setBean(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 <i>read-only</i>. * <p> * If the Binder is already bound to some bean, the newly bound field is * associated with the corresponding bean property as described above. * <p> * 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: * * <pre> * class Person { * public String getName() { ... } * public void setName(String name) { ... } * } * * TextField nameField = new TextField(); * binder.bind(nameField, Person::getName, Person::setName); * </pre> * * @param <FIELDVALUE> * 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 <FIELDVALUE> Binding<BEAN, FIELDVALUE> bind( HasValue<FIELDVALUE> field, ValueProvider<BEAN, FIELDVALUE> getter, Setter<BEAN, FIELDVALUE> 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}. * <p> * For a <code>Binder</code> 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. * <p> * 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 <i>read-only</i>. * * @param <FIELDVALUE> * 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 <FIELDVALUE> Binding<BEAN, FIELDVALUE> bind( HasValue<FIELDVALUE> 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. * <p> * 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. * <p> * Any change made in the fields also runs validation for the field * {@link Binding} and bean level validation for this binder (bean level * validators are added using {@link Binder#withValidator(Validator)}. * * @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; bindings.forEach(b -> b.initFieldValue(bean)); // 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. * <p> * 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. * <p> * 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(BEAN)}. * * @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 { setHasChanges(false); bindings.forEach(binding -> binding.initFieldValue(bean)); getValidationStatusHandler().statusChange( BinderValidationStatus.createUnresolvedStatus(this)); fireStatusChangeEvent(false); } } /** * Writes changes from the bound fields to the given bean if all validators * (binding and bean level) pass. * <p> * If any field binding validator fails, no values are written and a * {@code ValidationException} is thrown. * <p> * 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 { BinderValidationStatus<BEAN> status = doWriteIfValid(bean); if (status.hasErrors()) { throw new ValidationException(status.getFieldValidationErrors(), status.getBeanValidationErrors()); } } /** * Writes changes from the bound fields to the given bean if all validators * (binding and bean level) pass. * <p> * If any field binding validator fails, no values are written and * <code>false</code> is returned. * <p> * 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 <code>false</code> 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).isOk(); } /** * Writes the field values into the given bean if all field level validators * pass. Runs bean level validators on the bean after writing. * * @param bean * the bean to write field values into * @return a list of field validation errors if such occur, otherwise a list * of bean validation errors. */ @SuppressWarnings({ "rawtypes", "unchecked" }) private BinderValidationStatus<BEAN> doWriteIfValid(BEAN bean) { Objects.requireNonNull(bean, "bean cannot be null"); // First run fields level validation List<BindingValidationStatus<?>> bindingStatuses = validateBindings(); // If no validation errors then update bean if (bindingStatuses.stream().filter(BindingValidationStatus::isError) .findAny().isPresent()) { fireStatusChangeEvent(true); return new BinderValidationStatus<>(this, bindingStatuses, Collections.emptyList()); } // Store old bean values so we can restore them if validators fail Map<Binding<BEAN, ?>, Object> oldValues = new HashMap<>(); bindings.forEach( binding -> oldValues.put(binding, binding.getter.apply(bean))); bindings.forEach(binding -> binding.writeFieldValue(bean)); // Now run bean level validation against the updated bean List<ValidationResult> binderResults = validateBean(bean); boolean hasErrors = binderResults.stream() .filter(ValidationResult::isError).findAny().isPresent(); if (hasErrors) { // Bean validator failed, revert values bindings.forEach((BindingImpl binding) -> binding.setter .accept(bean, oldValues.get(binding))); } else { // Write successful, reset hasChanges to false setHasChanges(false); } fireStatusChangeEvent(hasErrors); return new BinderValidationStatus<>(this, bindingStatuses, binderResults); } /** * Adds an bean level validator. * <p> * 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<BEAN> withValidator(Validator<? super BEAN> validator) { Objects.requireNonNull(validator, "validator cannot be null"); validators.add(validator); return this; } /** * A convenience method to add a validator to this binder using the * {@link Validator#from(SerializablePredicate, String)} factory method. * <p> * 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<BEAN> withValidator(SerializablePredicate<BEAN> 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. * <p> * 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<BEAN> withValidator(SerializablePredicate<BEAN> 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()); if (hasChanges()) { fireStatusChangeEvent(false); } setHasChanges(false); } /** * Validates the values of all bound fields and returns the validation * status. * <p> * 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. * <p> * * @return validation status for the binder */ public BinderValidationStatus<BEAN> validate() { List<BindingValidationStatus<?>> bindingStatuses = validateBindings(); BinderValidationStatus<BEAN> validationStatus; if (bindingStatuses.stream().filter(BindingValidationStatus::isError) .findAny().isPresent() || bean == null) { validationStatus = new BinderValidationStatus<>(this, bindingStatuses, Collections.emptyList()); } else { validationStatus = new BinderValidationStatus<>(this, bindingStatuses, validateBean(bean)); } 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. * * @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() { if (getBean() == null && !validators.isEmpty()) { throw new IllegalStateException("Cannot validate binder: " + "bean level validators have been configured " + "but no bean is currently set"); } if (validateBindings().stream().filter(BindingValidationStatus::isError) .findAny().isPresent()) { return false; } if (getBean() != null && validateBean(getBean()).stream() .filter(ValidationResult::isError).findAny().isPresent()) { return false; } return true; } /** * Validates the bindings and returns the result of the validation as a list * of validation statuses. * <p> * Does not run bean validators. * * @see #validateBean(Object) * * @return an immutable list of validation results for bindings */ private List<BindingValidationStatus<?>> validateBindings() { List<BindingValidationStatus<?>> results = new ArrayList<>(); for (BindingImpl<?, ?, ?> binding : bindings) { results.add(binding.doValidation()); } return results; } /** * Validates the {@code bean} using validators added using * {@link #withValidator(Validator)} and returns the result of the * validation as a list of validation results. * <p> * * @see #withValidator(Validator) * * @param bean * the bean to validate * @return a list of validation errors or an empty list if validation * succeeded */ private List<ValidationResult> validateBean(BEAN bean) { Objects.requireNonNull(bean, "bean cannot be null"); List<ValidationResult> 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. * <p> * Only the one validation error message is shown in this label at a time. * <p> * 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<Label> getStatusLabel() { return Optional.ofNullable(statusLabel); } /** * Sets the status handler to track form status changes. * <p> * Setting this handler will override the default behavior, which is to let * fields show their validation status messages and show binder level * validation errors or OK status in the label set with * {@link #setStatusLabel(Label)}. * <p> * This handler cannot be set after the status label has been set with * {@link #setStatusLabel(Label)}, or {@link #setStatusLabel(Label)} cannot * be used after this handler has been set. * * @param statusHandler * the status handler to set, not <code>null</code> * @throws NullPointerException * for <code>null</code> status handler * @see #setStatusLabel(Label) * @see BindingBuilder#withValidationStatusHandler(BindingValidationStatusHandler) */ public void setValidationStatusHandler( BinderValidationStatusHandler<BEAN> statusHandler) { Objects.requireNonNull(statusHandler, "Cannot set a null " + BinderValidationStatusHandler.class.getSimpleName()); if (statusLabel != null) { throw new IllegalStateException("Cannot set " + BinderValidationStatusHandler.class.getSimpleName() + " if a status label has already been set."); } this.statusHandler = statusHandler; } /** * Gets the status handler of this form. * <p> * If none has been set with * {@link #setValidationStatusHandler(BinderValidationStatusHandler)}, the * default implementation is returned. * * @return the status handler used, never <code>null</code> * @see #setValidationStatusHandler(BinderValidationStatusHandler) */ public BinderValidationStatusHandler<BEAN> getValidationStatusHandler() { return Optional.ofNullable(statusHandler) .orElse(this::handleBinderValidationStatus); } /** * Adds status change listener to the binder. * <p> * The {@link Binder} status is changed whenever any of the following * happens: * <ul> * <li>if it's bound and any of its bound field or select has been changed * <li>{@link #writeBean(Object)} or {@link #writeBeanIfValid(Object)} is * called * <li>{@link #readBean(Object)} is called * <li>{@link #setBean(Object)} is called * <li>{@link #removeBean()} is called * <li>{@link BindingBuilder#bind(ValueProvider, Setter)} is called * <li>{@link Binder#validate()} or {@link Binding#validate()} is called * </ul> * * @see #readBean(Object) * @see #writeBean(Object) * @see #writeBeanIfValid(Object) * @see #setBean(Object) * @see #removeBean() * @see #forField(HasValue) * @see #validate() * @see Binding#validate() * * @param listener * status change listener to add, not null * @return a registration for the listener */ public Registration addStatusChangeListener(StatusChangeListener listener) { return getEventRouter().addListener(StatusChangeEvent.class, listener, StatusChangeListener.class.getDeclaredMethods()[0]); } /** * Adds field value change listener to all the fields in the binder. * <p> * Added listener is notified every time whenever any bound field value is * changed. The same functionality can be achieved by adding a * {@link ValueChangeListener} to all fields in the {@link Binder}. * <p> * The listener is added to all fields regardless of whether the method is * invoked before or after field is bound. * * @see ValueChangeEvent * @see ValueChangeListener * * @param listener * a field value change listener * @return a registration for the listener */ public Registration addValueChangeListener( ValueChangeListener<?> listener) { return getEventRouter().addListener(ValueChangeEvent.class, listener, ValueChangeListener.class.getDeclaredMethods()[0]); } /** * Creates a new binding with the given field. * * @param <FIELDVALUE> * the value type of the field * @param <TARGET> * the target data type * @param field * the field to bind, not null * @param converter * the converter for converting between FIELDVALUE and TARGET * types, not null * @param handler * the handler to notify of status changes, not null * @return the new incomplete binding */ protected <FIELDVALUE, TARGET> BindingBuilder<BEAN, TARGET> createBinding( HasValue<FIELDVALUE> field, Converter<FIELDVALUE, TARGET> converter, BindingValidationStatusHandler handler) { BindingBuilder<BEAN, TARGET> newBinding = doCreateBinding(field, converter, handler); if (incompleteMemberFieldBindings.containsKey(field)) { incompleteMemberFieldBindings.put(field, newBinding); } incompleteBindings.put(field, newBinding); return newBinding; } protected <FIELDVALUE, TARGET> BindingBuilder<BEAN, TARGET> doCreateBinding( HasValue<FIELDVALUE> field, Converter<FIELDVALUE, TARGET> converter, BindingValidationStatusHandler handler) { return new BindingBuilderImpl<>(this, field, converter, handler); } /** * Clears the error condition of the given field, if any. The default * implementation clears the * {@link AbstractComponent#setComponentError(ErrorMessage) component error} * of the field if it is a Component, otherwise does nothing. * * @param field * the field with an invalid value */ protected void clearError(HasValue<?> field) { if (field instanceof AbstractComponent) { ((AbstractComponent) field).setComponentError(null); } } /** * Handles a validation error emitted when trying to write the value of the * given field. The default implementation sets the * {@link AbstractComponent#setComponentError(ErrorMessage) component error} * of the field if it is a Component, otherwise does nothing. * * @param field * the field with the invalid value * @param error * the error message to set */ protected void handleError(HasValue<?> field, String error) { if (field instanceof AbstractComponent) { ((AbstractComponent) field).setComponentError(new UserError(error)); } } /** * Default {@link BindingValidationStatusHandler} functional method * implementation. * * @param status * the validation status */ protected void handleValidationStatus(BindingValidationStatus<?> status) { HasValue<?> source = status.getField(); clearError(source); if (status.isError()) { handleError(source, status.getMessage().get()); } } /** * Returns the bindings for this binder. * * @return a set of the bindings */ protected Set<BindingImpl<BEAN, ?, ?>> getBindings() { return bindings; } /** * The default binder level status handler. * <p> * Passes all field related results to the Binding status handlers. All * other status changes are displayed in the status label, if one has been * set with {@link #setStatusLabel(Label)}. * * @param binderStatus * status of validation results from binding and/or bean level * validators */ protected void handleBinderValidationStatus( BinderValidationStatus<BEAN> binderStatus) { // let field events go to binding status handlers binderStatus.getFieldValidationStatuses() .forEach(status -> ((BindingImpl<?, ?, ?>) status.getBinding()) .notifyStatusHandler(status)); // show first possible error or OK status in the label if set if (getStatusLabel().isPresent()) { String statusMessage = binderStatus.getBeanValidationErrors() .stream().findFirst().map(ValidationResult::getErrorMessage) .orElse(""); getStatusLabel().get().setValue(statusMessage); } } /** * Sets whether the values of the fields this binder is bound to have * changed since the last explicit call to either bind, write or read. * * @param hasChanges * whether this binder should be marked to have changes */ private void setHasChanges(boolean hasChanges) { this.hasChanges = hasChanges; } /** * Check whether any of the bound fields' have uncommitted changes since * last explicit call to {@link #readBean(Object)}, {@link #removeBean()}, * {@link #writeBean(Object)} or {@link #writeBeanIfValid(Object)}. * Unsuccessful write operations will not affect this value. * <p> * Note that if you use {@link #setBean(Object)} method, Binder tries to * commit changes as soon as all validators have passed. Thus, when using * this method with it seldom makes sense and almost always returns false. * * Return values for each case are compiled into the following table: * * <p> * * <table> * <tr> * <td></td> * <td>After readBean, setBean or removeBean</td> * <td>After valid user changes</td> * <td>After invalid user changes</td> * <td>After successful writeBean or writeBeanIfValid</td> * <td>After unsuccessful writeBean or writeBeanIfValid</td> * </tr> * <tr> * <td>A bean is currently bound</td> * <td>{@code false}</td> * <td>{@code false}</td> * <td>{@code true}</td> * <td>{@code false}</td> * <td>no change</td> * </tr> * <tr> * <td>No bean is currently bound</td> * <td>{@code false}</td> * <td>{@code true}</td> * <td>{@code true}</td> * <td>{@code false}</td> * <td>no change</td> * </tr> * </table> * * @return whether any bound field's value has changed since last call to * setBean, readBean, writeBean or writeBeanIfValid */ public boolean hasChanges() { return hasChanges; } /** * Sets the read only state to the given value for all currently bound * fields. * <p> * This is just a shorthand for calling setReadOnly for all currently bound * fields. It means that fields bound after this method call won't be set * read-only. * * @param fieldsReadOnly * true to set the fields to read only, false to set them to read * write */ public void setReadOnly(boolean fieldsReadOnly) { getBindings().stream().map(BindingImpl::getField) .forEach(field -> field.setReadOnly(fieldsReadOnly)); } /** * Returns the event router for this binder. * * @return the event router, not null */ protected EventRouter getEventRouter() { if (eventRouter == null) { eventRouter = new EventRouter(); } return eventRouter; } /** * Configures the {@code binding} with the property definition * {@code definition} before it's being bound. * * @param binding * a binding to configure * @param definition * a property definition information * @return the new configured binding */ protected BindingBuilder<BEAN, ?> configureBinding( BindingBuilder<BEAN, ?> binding, PropertyDefinition<BEAN, ?> definition) { return binding; } private void doRemoveBean(boolean fireStatusEvent) { setHasChanges(false); if (bean != null) { bean = null; } getValidationStatusHandler().statusChange( BinderValidationStatus.createUnresolvedStatus(this)); if (fireStatusEvent) { fireStatusChangeEvent(false); } } private void fireStatusChangeEvent(boolean hasValidationErrors) { getEventRouter() .fireEvent(new StatusChangeEvent(this, hasValidationErrors)); } private <FIELDVALUE> Converter<FIELDVALUE, FIELDVALUE> createNullRepresentationAdapter( HasValue<FIELDVALUE> field) { Converter<FIELDVALUE, FIELDVALUE> nullRepresentationConverter = Converter .from(fieldValue -> fieldValue, modelValue -> Objects.isNull(modelValue) ? field.getEmptyValue() : modelValue, exception -> exception.getMessage()); ConverterDelegate<FIELDVALUE> converter = new ConverterDelegate<>( nullRepresentationConverter); initialConverters.put(field, converter); return converter; } /** * Throws if this binder has incomplete bindings. * * @param methodName * name of the method where this call is originated from * @throws IllegalStateException * if this binder has incomplete bindings */ private void checkBindingsCompleted(String methodName) { if (!incompleteMemberFieldBindings.isEmpty()) { throw new IllegalStateException( "All bindings created with forMemberField must " + "be completed with bindInstanceFields before calling " + methodName); } if (!incompleteBindings.isEmpty()) { throw new IllegalStateException( "All bindings created with forField must be completed before calling " + methodName); } } /** * Binds member fields found in the given object. * <p> * This method processes all (Java) member fields whose type extends * {@link HasValue} and that can be mapped to a property id. Property name * mapping is done based on the field name or on a @{@link PropertyId} * annotation on the field. All non-null unbound fields for which a property * name can be determined are bound to the property name using * {@link BindingBuilder#bind(String)}. * <p> * For example: * * <pre> * public class MyForm extends VerticalLayout { * private TextField firstName = new TextField("First name"); * @PropertyId("last") * private TextField lastName = new TextField("Last name"); * * MyForm myForm = new MyForm(); * ... * binder.bindMemberFields(myForm); * </pre> * * This binds the firstName TextField to a "firstName" property in the item, * lastName TextField to a "last" property. * <p> * It's not always possible to bind a field to a property because their * types are incompatible. E.g. custom converter is required to bind * {@code HasValue<String>} and {@code Integer} property (that would be a * case of "age" property). In such case {@link IllegalStateException} will * be thrown unless the field has been configured manually before calling * the {@link #bindInstanceFields(Object)} method. * <p> * It's always possible to do custom binding for any field: the * {@link #bindInstanceFields(Object)} method doesn't override existing * bindings. * * @param objectWithMemberFields * The object that contains (Java) member fields to bind * @throws IllegalStateException * if there are incompatible HasValue<T> and property * types */ public void bindInstanceFields(Object objectWithMemberFields) { Class<?> objectClass = objectWithMemberFields.getClass(); Integer numberOfBoundFields = getFieldsInDeclareOrder(objectClass) .stream() .filter(memberField -> HasValue.class .isAssignableFrom(memberField.getType())) .filter(memberField -> !isFieldBound(memberField, objectWithMemberFields)) .map(memberField -> handleProperty(memberField, objectWithMemberFields, (property, type) -> bindProperty(objectWithMemberFields, memberField, property, type))) .reduce(0, this::accumulate, Integer::sum); if (numberOfBoundFields == 0) { throw new IllegalStateException("There are no instance fields " + "found for automatic binding"); } } private boolean isFieldBound(Field memberField, Object objectWithMemberFields) { try { HasValue field = (HasValue) getMemberFieldValue(memberField, objectWithMemberFields); return bindings.stream() .anyMatch(binding -> binding.getField() == field); } catch (Exception e) { return false; } } private int accumulate(int count, boolean value) { return value ? count + 1 : count; } private BindingBuilder<BEAN, ?> getIncompleteMemberFieldBinding( Field memberField, Object objectWithMemberFields) { return incompleteMemberFieldBindings .get(getMemberFieldValue(memberField, objectWithMemberFields)); } private Object getMemberFieldValue(Field memberField, Object objectWithMemberFields) { memberField.setAccessible(true); try { return memberField.get(objectWithMemberFields); } catch (IllegalArgumentException | IllegalAccessException e) { throw new RuntimeException(e); } finally { memberField.setAccessible(false); } } /** * Binds {@code property} with {@code propertyType} to the field in the * {@code objectWithMemberFields} instance using {@code memberField} as a * reference to a member. * * @param objectWithMemberFields * the object that contains (Java) member fields to build and * bind * @param memberField * reference to a member field to bind * @param property * property name to bind * @param propertyType * type of the property * @return {@code true} if property is successfully bound */ private boolean bindProperty(Object objectWithMemberFields, Field memberField, String property, Class<?> propertyType) { Type valueType = GenericTypeReflector.getTypeParameter( memberField.getGenericType(), HasValue.class.getTypeParameters()[0]); if (valueType == null) { throw new IllegalStateException(String.format( "Unable to detect value type for the member '%s' in the " + "class '%s'.", memberField.getName(), objectWithMemberFields.getClass().getName())); } if (propertyType.equals(GenericTypeReflector.erase(valueType))) { HasValue<?> field; // Get the field from the object try { field = (HasValue<?>) ReflectTools.getJavaFieldValue( objectWithMemberFields, memberField, HasValue.class); } catch (IllegalArgumentException | IllegalAccessException | InvocationTargetException e) { // If we cannot determine the value, just skip the field return false; } if (field == null) { field = makeFieldInstance( (Class<? extends HasValue<?>>) memberField.getType()); initializeField(objectWithMemberFields, memberField, field); } forField(field).bind(property); return true; } else { throw new IllegalStateException(String.format( "Property type '%s' doesn't " + "match the field type '%s'. " + "Binding should be configured manually using converter.", propertyType.getName(), valueType.getTypeName())); } } /** * Makes an instance of the field type {@code fieldClass}. * <p> * The resulting field instance is used to bind a property to it using the * {@link #bindInstanceFields(Object)} method. * <p> * The default implementation relies on the default constructor of the * class. If there is no suitable default constructor or you want to * configure the instantiated class then override this method and provide * your own implementation. * * @see #bindInstanceFields(Object) * @param fieldClass * type of the field * @return a {@code fieldClass} instance object */ private HasValue<?> makeFieldInstance( Class<? extends HasValue<?>> fieldClass) { try { return fieldClass.newInstance(); } catch (InstantiationException | IllegalAccessException e) { throw new IllegalStateException( String.format("Couldn't create an '%s' type instance", fieldClass.getName()), e); } } /** * Returns an array containing {@link Field} objects reflecting all the * fields of the class or interface represented by this Class object. The * elements in the array returned are sorted in declare order from sub class * to super class. * * @param searchClass * class to introspect * @return list of all fields in the class considering hierarchy */ private List<Field> getFieldsInDeclareOrder(Class<?> searchClass) { ArrayList<Field> memberFieldInOrder = new ArrayList<>(); while (searchClass != null) { memberFieldInOrder .addAll(Arrays.asList(searchClass.getDeclaredFields())); searchClass = searchClass.getSuperclass(); } return memberFieldInOrder; } private void initializeField(Object objectWithMemberFields, Field memberField, HasValue<?> value) { try { ReflectTools.setJavaFieldValue(objectWithMemberFields, memberField, value); } catch (IllegalArgumentException | IllegalAccessException | InvocationTargetException e) { throw new IllegalStateException( String.format("Could not assign value to field '%s'", memberField.getName()), e); } } private boolean handleProperty(Field field, Object objectWithMemberFields, BiFunction<String, Class<?>, Boolean> propertyHandler) { Optional<PropertyDefinition<BEAN, ?>> descriptor = getPropertyDescriptor( field); if (!descriptor.isPresent()) { return false; } String propertyName = descriptor.get().getName(); if (boundProperties.containsKey(propertyName)) { return false; } BindingBuilder<BEAN, ?> tentativeBinding = getIncompleteMemberFieldBinding( field, objectWithMemberFields); if (tentativeBinding != null) { tentativeBinding.bind(propertyName); return false; } Boolean isPropertyBound = propertyHandler.apply(propertyName, descriptor.get().getType()); assert boundProperties.containsKey(propertyName); return isPropertyBound; } /** * Gets the binding for a property name. Bindings are available by property * name if bound using {@link #bind(HasValue, String)}, * {@link BindingBuilder#bind(String)} or indirectly using * {@link #bindInstanceFields(Object)}. * * @param propertyName * the property name of the binding to get * @return the binding corresponding to the property name, or an empty * optional if there is no binding with that property name */ public Optional<Binding<BEAN, ?>> getBinding(String propertyName) { return Optional.ofNullable(boundProperties.get(propertyName)); } private Optional<PropertyDefinition<BEAN, ?>> getPropertyDescriptor( Field field) { PropertyId propertyIdAnnotation = field.getAnnotation(PropertyId.class); String propertyId; if (propertyIdAnnotation != null) { // @PropertyId(propertyId) always overrides property id propertyId = propertyIdAnnotation.value(); } else { propertyId = field.getName(); } String minifiedFieldName = minifyFieldName(propertyId); return propertySet.getProperties().map(PropertyDefinition::getName) .filter(name -> minifyFieldName(name).equals(minifiedFieldName)) .findFirst().flatMap(propertySet::getProperty); } private String minifyFieldName(String fieldName) { return fieldName.toLowerCase(Locale.ENGLISH).replace("_", ""); } private <V> void fireValueChangeEvent(ValueChangeEvent<V> event) { getEventRouter().fireEvent(event); } }