Change-Id: Ibf1223d4842d72f0209231dfd70e1d6c4deb6d30tags/8.0.0.alpha1
Slider salaryLevelField = new Slider("Salary level", 1, 10); | Slider salaryLevelField = new Slider("Salary level", 1, 10); | ||||
binder.forField(salaryLevelField) | binder.forField(salaryLevelField) | ||||
.withConverter(Integer::doubleValue, Double::intValue) | |||||
.withConverter(Double::intValue, Integer::doubleValue) | |||||
.bind(Person::getSalaryLevel, Person::setSalaryLevel); | .bind(Person::getSalaryLevel, Person::setSalaryLevel); | ||||
---- | ---- |
import java.util.ArrayList; | import java.util.ArrayList; | ||||
import java.util.LinkedHashSet; | import java.util.LinkedHashSet; | ||||
import java.util.List; | import java.util.List; | ||||
import java.util.Locale; | |||||
import java.util.Objects; | import java.util.Objects; | ||||
import java.util.Optional; | import java.util.Optional; | ||||
import java.util.Set; | import java.util.Set; | ||||
import java.util.function.BiConsumer; | import java.util.function.BiConsumer; | ||||
import java.util.function.Function; | import java.util.function.Function; | ||||
import java.util.function.Predicate; | import java.util.function.Predicate; | ||||
import java.util.stream.Collectors; | |||||
import com.vaadin.data.util.converter.Converter; | |||||
import com.vaadin.data.util.converter.StringToIntegerConverter; | |||||
import com.vaadin.event.Registration; | import com.vaadin.event.Registration; | ||||
import com.vaadin.server.ErrorMessage; | |||||
import com.vaadin.server.UserError; | import com.vaadin.server.UserError; | ||||
import com.vaadin.ui.AbstractComponent; | import com.vaadin.ui.AbstractComponent; | ||||
public Binding<BEAN, FIELDVALUE, TARGET> withValidator( | public Binding<BEAN, FIELDVALUE, TARGET> withValidator( | ||||
Predicate<? super TARGET> predicate, String message); | Predicate<? super TARGET> predicate, String message); | ||||
/** | |||||
* 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(Function, BiConsumer)}, 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> Binding<BEAN, FIELDVALUE, 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(Function, BiConsumer)}, 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 | |||||
*/ | |||||
default public <NEWTARGET> Binding<BEAN, FIELDVALUE, NEWTARGET> withConverter( | |||||
Function<TARGET, NEWTARGET> toModel, | |||||
Function<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(Function, BiConsumer)}, 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> Binding<BEAN, FIELDVALUE, NEWTARGET> withConverter( | |||||
Function<TARGET, NEWTARGET> toModel, | |||||
Function<NEWTARGET, TARGET> toPresentation, | |||||
String errorMessage) { | |||||
return withConverter(Converter.from(toModel, toPresentation, | |||||
exception -> errorMessage)); | |||||
} | |||||
/** | /** | ||||
* Gets the field the binding uses. | * Gets the field the binding uses. | ||||
* | * | ||||
protected static class BindingImpl<BEAN, FIELDVALUE, TARGET> | protected static class BindingImpl<BEAN, FIELDVALUE, TARGET> | ||||
implements Binding<BEAN, FIELDVALUE, TARGET> { | implements Binding<BEAN, FIELDVALUE, TARGET> { | ||||
private Binder<BEAN> binder; | |||||
private final Binder<BEAN> binder; | |||||
private HasValue<FIELDVALUE> field; | |||||
private final HasValue<FIELDVALUE> field; | |||||
private Registration onValueChange; | private Registration onValueChange; | ||||
private Function<BEAN, TARGET> getter; | private Function<BEAN, TARGET> getter; | ||||
private BiConsumer<BEAN, TARGET> setter; | private BiConsumer<BEAN, TARGET> setter; | ||||
private List<Validator<? super TARGET>> validators = new ArrayList<>(); | |||||
/** | |||||
* Contains all converters and validators chained together in the | |||||
* correct order. | |||||
*/ | |||||
private Converter<FIELDVALUE, TARGET> converterValidatorChain; | |||||
/** | /** | ||||
* Creates a new binding associated with the given field. | * Creates a new binding associated with the given field. | ||||
* @param field | * @param field | ||||
* the field to bind | * the field to bind | ||||
*/ | */ | ||||
@SuppressWarnings("unchecked") | |||||
protected BindingImpl(Binder<BEAN> binder, HasValue<FIELDVALUE> field) { | protected BindingImpl(Binder<BEAN> binder, HasValue<FIELDVALUE> field) { | ||||
this.binder = binder; | |||||
this(binder, field, | |||||
(Converter<FIELDVALUE, TARGET>) Converter.identity()); | |||||
} | |||||
/** | |||||
* Creates a new binding associated with the given field using the given | |||||
* converter chain. | |||||
* | |||||
* @param binder | |||||
* the binder this instance is connected to | |||||
* @param field | |||||
* the field to bind | |||||
* @param converterValidatorChain | |||||
* the converter/validator chain to use | |||||
*/ | |||||
protected BindingImpl(Binder<BEAN> binder, HasValue<FIELDVALUE> field, | |||||
Converter<FIELDVALUE, TARGET> converterValidatorChain) { | |||||
this.field = field; | this.field = field; | ||||
this.binder = binder; | |||||
this.converterValidatorChain = converterValidatorChain; | |||||
} | } | ||||
@Override | @Override | ||||
Validator<? super TARGET> validator) { | Validator<? super TARGET> validator) { | ||||
checkUnbound(); | checkUnbound(); | ||||
Objects.requireNonNull(validator, "validator cannot be null"); | Objects.requireNonNull(validator, "validator cannot be null"); | ||||
validators.add(validator); | |||||
Converter<TARGET, TARGET> validatorAsConverter = new ValidatorAsConverter<>( | |||||
validator); | |||||
converterValidatorChain = converterValidatorChain | |||||
.chain(validatorAsConverter); | |||||
return this; | return this; | ||||
} | } | ||||
return withValidator(Validator.from(predicate, message)); | return withValidator(Validator.from(predicate, message)); | ||||
} | } | ||||
@Override | |||||
public <NEWTARGET> Binding<BEAN, FIELDVALUE, NEWTARGET> withConverter( | |||||
Converter<TARGET, NEWTARGET> converter) { | |||||
checkUnbound(); | |||||
Objects.requireNonNull(converter, "converter cannot be null"); | |||||
BindingImpl<BEAN, FIELDVALUE, NEWTARGET> newBinding = new BindingImpl<>( | |||||
binder, field, converterValidatorChain.chain(converter)); | |||||
return newBinding; | |||||
} | |||||
private void bind(BEAN bean) { | private void bind(BEAN bean) { | ||||
setFieldValue(bean); | setFieldValue(bean); | ||||
onValueChange = field | onValueChange = field | ||||
.addValueChangeListener(e -> storeFieldValue(bean)); | .addValueChangeListener(e -> storeFieldValue(bean)); | ||||
} | } | ||||
private List<ValidationError<FIELDVALUE>> validate() { | |||||
return validators.stream() | |||||
.map(validator -> validator | |||||
.apply((TARGET) field.getValue())) | |||||
.filter(Result::isError) | |||||
.map(result -> new ValidationError<>(field, | |||||
result.getMessage().orElse(null))) | |||||
.collect(Collectors.toList()); | |||||
private Result<TARGET> validate() { | |||||
FIELDVALUE fieldValue = field.getValue(); | |||||
Result<TARGET> dataValue = converterValidatorChain.convertToModel( | |||||
fieldValue, ((AbstractComponent) field).getLocale()); | |||||
return dataValue; | |||||
} | |||||
/** | |||||
* Returns the field value run through all converters and validators. | |||||
* | |||||
* @return an optional containing the validated and converted value or | |||||
* an empty optional if a validator or converter failed | |||||
*/ | |||||
private Optional<TARGET> getTargetValue() { | |||||
return validate().getValue(); | |||||
} | } | ||||
private void unbind() { | private void unbind() { | ||||
*/ | */ | ||||
private void setFieldValue(BEAN bean) { | private void setFieldValue(BEAN bean) { | ||||
assert bean != null; | assert bean != null; | ||||
field.setValue((FIELDVALUE) getter.apply(bean)); | |||||
field.setValue(convertDataToFieldType(bean)); | |||||
} | |||||
private FIELDVALUE convertDataToFieldType(BEAN bean) { | |||||
return converterValidatorChain.convertToPresentation( | |||||
getter.apply(bean), | |||||
((AbstractComponent) field).getLocale()); | |||||
} | } | ||||
/** | /** | ||||
private void storeFieldValue(BEAN bean) { | private void storeFieldValue(BEAN bean) { | ||||
assert bean != null; | assert bean != null; | ||||
if (setter != null) { | if (setter != null) { | ||||
setter.accept(bean, (TARGET) field.getValue()); | |||||
getTargetValue().ifPresent(value -> setter.accept(bean, value)); | |||||
} | } | ||||
} | } | ||||
private void checkUnbound() { | private void checkUnbound() { | ||||
if (this.getter != null) { | |||||
if (getter != null) { | |||||
throw new IllegalStateException( | throw new IllegalStateException( | ||||
"cannot modify binding: already bound to a property"); | "cannot modify binding: already bound to a property"); | ||||
} | } | ||||
} | } | ||||
} | } | ||||
/** | |||||
* 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 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, Locale locale) { | |||||
Result<? super T> validationResult = validator.apply(value); | |||||
if (validationResult.isError()) { | |||||
return Result.error(validationResult.getMessage().get()); | |||||
} else { | |||||
return Result.ok(value); | |||||
} | |||||
} | |||||
@Override | |||||
public T convertToPresentation(T value, Locale locale) { | |||||
return value; | |||||
} | |||||
} | |||||
private BEAN bean; | private BEAN bean; | ||||
private Set<BindingImpl<BEAN, ?, ?>> bindings = new LinkedHashSet<>(); | private Set<BindingImpl<BEAN, ?, ?>> bindings = new LinkedHashSet<>(); | ||||
public List<ValidationError<?>> validate() { | public List<ValidationError<?>> validate() { | ||||
List<ValidationError<?>> resultErrors = new ArrayList<>(); | List<ValidationError<?>> resultErrors = new ArrayList<>(); | ||||
for (BindingImpl<BEAN, ?, ?> binding : bindings) { | for (BindingImpl<BEAN, ?, ?> binding : bindings) { | ||||
clearError(binding.getField()); | |||||
List<? extends ValidationError<?>> errors = binding.validate(); | |||||
resultErrors.addAll(errors); | |||||
if (!errors.isEmpty()) { | |||||
handleError(binding.getField(), errors.get(0).getMessage()); | |||||
} | |||||
clearError(binding.field); | |||||
binding.validate().ifError(errorMessage -> { | |||||
resultErrors.add( | |||||
new ValidationError<>(binding.field, errorMessage)); | |||||
handleError(binding.field, errorMessage); | |||||
}); | |||||
} | } | ||||
return resultErrors; | return resultErrors; | ||||
} | } | ||||
* the field to bind | * the field to bind | ||||
* @return the new incomplete binding | * @return the new incomplete binding | ||||
*/ | */ | ||||
protected <FIELDVALUE> Binding<BEAN, FIELDVALUE, FIELDVALUE> createBinding( | |||||
protected <FIELDVALUE> BindingImpl<BEAN, FIELDVALUE, FIELDVALUE> createBinding( | |||||
HasValue<FIELDVALUE> field) { | HasValue<FIELDVALUE> field) { | ||||
Objects.requireNonNull(field, "field cannot be null"); | Objects.requireNonNull(field, "field cannot be null"); | ||||
BindingImpl<BEAN, FIELDVALUE, FIELDVALUE> b = new BindingImpl<>(this, | |||||
field); | |||||
BindingImpl<BEAN, FIELDVALUE, FIELDVALUE> b = new BindingImpl<BEAN, FIELDVALUE, FIELDVALUE>( | |||||
this, field); | |||||
return b; | return b; | ||||
} | } | ||||
/* | /* | ||||
* Copyright 2000-2014 Vaadin Ltd. | * Copyright 2000-2014 Vaadin Ltd. | ||||
* | |||||
* | |||||
* Licensed under the Apache License, Version 2.0 (the "License"); you may not | * 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 | * use this file except in compliance with the License. You may obtain a copy of | ||||
* the License at | * the License at | ||||
* | |||||
* | |||||
* http://www.apache.org/licenses/LICENSE-2.0 | * http://www.apache.org/licenses/LICENSE-2.0 | ||||
* | |||||
* | |||||
* Unless required by applicable law or agreed to in writing, software | * Unless required by applicable law or agreed to in writing, software | ||||
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT | * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT | ||||
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the | * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the | ||||
/** | /** | ||||
* Returns a successful result wrapping the given value. | * Returns a successful result wrapping the given value. | ||||
* | |||||
* | |||||
* @param <R> | * @param <R> | ||||
* the result value type | * the result value type | ||||
* @param value | * @param value | ||||
/** | /** | ||||
* Returns a failure result wrapping the given error message. | * Returns a failure result wrapping the given error message. | ||||
* | |||||
* | |||||
* @param <R> | * @param <R> | ||||
* the result value type | * the result value type | ||||
* @param message | * @param message | ||||
* If the supplier returns a value, returns a {@code Result.ok} of the | * If the supplier returns a value, returns a {@code Result.ok} of the | ||||
* value; if an exception is thrown, returns the message in a | * value; if an exception is thrown, returns the message in a | ||||
* {@code Result.error}. | * {@code Result.error}. | ||||
* | |||||
* | |||||
* @param <R> | * @param <R> | ||||
* the result value type | * the result value type | ||||
* @param supplier | * @param supplier | ||||
* function to the value. Otherwise, returns a Result bearing the same error | * function to the value. Otherwise, returns a Result bearing the same error | ||||
* as this one. Note that any exceptions thrown by the mapping function are | * as this one. Note that any exceptions thrown by the mapping function are | ||||
* not wrapped but allowed to propagate. | * not wrapped but allowed to propagate. | ||||
* | |||||
* | |||||
* @param <S> | * @param <S> | ||||
* the type of the mapped value | * the type of the mapped value | ||||
* @param mapper | * @param mapper | ||||
* to the value. Otherwise, returns a Result bearing the same error as this | * to the value. Otherwise, returns a Result bearing the same error as this | ||||
* one. Note that any exceptions thrown by the mapping function are not | * one. Note that any exceptions thrown by the mapping function are not | ||||
* wrapped but allowed to propagate. | * wrapped but allowed to propagate. | ||||
* | |||||
* | |||||
* @param <S> | * @param <S> | ||||
* the type of the mapped value | * the type of the mapped value | ||||
* @param mapper | * @param mapper | ||||
/** | /** | ||||
* Invokes either the first callback or the second one, depending on whether | * Invokes either the first callback or the second one, depending on whether | ||||
* this Result denotes a success or a failure, respectively. | * this Result denotes a success or a failure, respectively. | ||||
* | |||||
* | |||||
* @param ifOk | * @param ifOk | ||||
* the function to call if success | * the function to call if success | ||||
* @param ifError | * @param ifError | ||||
/** | /** | ||||
* Applies the {@code consumer} if result is not an error. | * Applies the {@code consumer} if result is not an error. | ||||
* | |||||
* | |||||
* @param consumer | * @param consumer | ||||
* consumer to apply in case it's not an error | * consumer to apply in case it's not an error | ||||
*/ | */ | ||||
/** | /** | ||||
* Applies the {@code consumer} if result is an error. | * Applies the {@code consumer} if result is an error. | ||||
* | |||||
* | |||||
* @param consumer | * @param consumer | ||||
* consumer to apply in case it's an error | * consumer to apply in case it's an error | ||||
*/ | */ | ||||
/** | /** | ||||
* Returns {@code true} if result is an error. | * Returns {@code true} if result is an error. | ||||
* | |||||
* | |||||
* @return whether the result is an error | * @return whether the result is an error | ||||
*/ | */ | ||||
public boolean isError(); | public boolean isError(); | ||||
/** | /** | ||||
* Returns an Optional of the result message, or an empty Optional if none. | * Returns an Optional of the result message, or an empty Optional if none. | ||||
* | |||||
* | |||||
* @return the optional message | * @return the optional message | ||||
*/ | */ | ||||
public Optional<String> getMessage(); | public Optional<String> getMessage(); | ||||
/** | |||||
* Returns an Optional of the value, or an empty Optional if none. | |||||
* | |||||
* @return the optional value | |||||
*/ | |||||
public Optional<R> getValue(); | |||||
} | } |
* <p> | * <p> | ||||
* If {@code message} is null then {@code value} is ignored and result is an | * If {@code message} is null then {@code value} is ignored and result is an | ||||
* error. | * error. | ||||
* | |||||
* | |||||
* @param value | * @param value | ||||
* the value of the result, may be {@code null} | * the value of the result, may be {@code null} | ||||
* @param message | * @param message | ||||
return Optional.ofNullable(message); | return Optional.ofNullable(message); | ||||
} | } | ||||
@Override | |||||
public Optional<R> getValue() { | |||||
return Optional.ofNullable(value); | |||||
} | |||||
@Override | @Override | ||||
public boolean isError() { | public boolean isError() { | ||||
return message != null; | return message != null; | ||||
return "ok(" + value + ")"; | return "ok(" + value + ")"; | ||||
} | } | ||||
} | } | ||||
} | } |
/* | |||||
* Copyright 2000-2014 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.util.converter; | |||||
import java.text.NumberFormat; | |||||
import java.text.ParsePosition; | |||||
import java.util.Locale; | |||||
import com.vaadin.legacy.data.util.converter.LegacyConverter.ConversionException; | |||||
/** | |||||
* A converter that converts from the number type T to {@link String} and back. | |||||
* Uses the given locale and {@link NumberFormat} for formatting and parsing. | |||||
* Automatically trims the input string, removing any leading and trailing white | |||||
* space. | |||||
* <p> | |||||
* Override and overwrite {@link #getFormat(Locale)} to use a different format. | |||||
* </p> | |||||
* | |||||
* @author Vaadin Ltd | |||||
* @since 8.0 | |||||
*/ | |||||
public abstract class AbstractStringToNumberConverter<T> | |||||
implements Converter<String, T> { | |||||
/** | |||||
* Returns the format used by {@link #convertToPresentation(Object, Locale)} | |||||
* and {@link #convertToModel(Object, Locale)}. | |||||
* | |||||
* @param locale | |||||
* The locale to use | |||||
* @return A NumberFormat instance | |||||
*/ | |||||
protected NumberFormat getFormat(Locale locale) { | |||||
if (locale == null) { | |||||
locale = Locale.getDefault(); | |||||
} | |||||
return NumberFormat.getNumberInstance(locale); | |||||
} | |||||
/** | |||||
* Convert the value to a Number using the given locale and | |||||
* {@link #getFormat(Locale)}. | |||||
* | |||||
* @param value | |||||
* The value to convert | |||||
* @param locale | |||||
* The locale to use for conversion | |||||
* @return The converted value | |||||
* @throws ConversionException | |||||
* If there was a problem converting the value | |||||
*/ | |||||
protected Number convertToNumber(String value, Locale locale) | |||||
throws ConversionException { | |||||
if (value == null) { | |||||
return null; | |||||
} | |||||
// Remove leading and trailing white space | |||||
value = value.trim(); | |||||
// Parse and detect errors. If the full string was not used, it is | |||||
// an error. | |||||
ParsePosition parsePosition = new ParsePosition(0); | |||||
Number parsedValue = getFormat(locale).parse(value, parsePosition); | |||||
if (parsePosition.getIndex() != value.length()) { | |||||
throw new ConversionException("Could not convert '" + value + "'"); | |||||
} | |||||
if (parsedValue == null) { | |||||
// Convert "" to null | |||||
return null; | |||||
} | |||||
return parsedValue; | |||||
} | |||||
@Override | |||||
public String convertToPresentation(T value, Locale locale) { | |||||
if (value == null) { | |||||
return null; | |||||
} | |||||
return getFormat(locale).format(value); | |||||
} | |||||
} |
/* | |||||
* Copyright 2000-2014 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.util.converter; | |||||
import java.io.Serializable; | |||||
import java.util.Locale; | |||||
import java.util.function.Function; | |||||
import com.vaadin.data.Binder.Binding; | |||||
import com.vaadin.data.Result; | |||||
import com.vaadin.legacy.data.util.converter.LegacyConverter.ConversionException; | |||||
/** | |||||
* Interface that implements conversion between a model and a presentation type. | |||||
* <p> | |||||
* Converters must not have any side effects (never update UI from inside a | |||||
* converter). | |||||
* | |||||
* @param <PRESENTATION> | |||||
* The presentation type. | |||||
* @param <MODEL> | |||||
* The model type. | |||||
* @author Vaadin Ltd. | |||||
* @since 8.0 | |||||
*/ | |||||
public interface Converter<PRESENTATION, MODEL> extends Serializable { | |||||
/** | |||||
* Converts the given value from model type to presentation type. | |||||
* <p> | |||||
* A converter can optionally use locale to do the conversion. | |||||
* | |||||
* @param value | |||||
* The value to convert. Can be null | |||||
* @param locale | |||||
* The locale to use for conversion. Can be null. | |||||
* @return The converted value compatible with the source type | |||||
*/ | |||||
public Result<MODEL> convertToModel(PRESENTATION value, Locale locale); | |||||
/** | |||||
* Converts the given value from presentation type to model type. | |||||
* <p> | |||||
* A converter can optionally use locale to do the conversion. | |||||
* | |||||
* @param value | |||||
* The value to convert. Can be null | |||||
* @param locale | |||||
* The locale to use for conversion. Can be null. | |||||
* @return The converted value compatible with the source type | |||||
*/ | |||||
public PRESENTATION convertToPresentation(MODEL value, Locale locale); | |||||
/** | |||||
* Returns a converter that returns its input as-is in both directions. | |||||
* | |||||
* @param <T> | |||||
* the input and output type | |||||
* @return an identity converter | |||||
*/ | |||||
public static <T> Converter<T, T> identity() { | |||||
return from(t -> Result.ok(t), t -> t); | |||||
} | |||||
/** | |||||
* Constructs a converter from two functions. Any {@code Exception} | |||||
* instances thrown from the {@code toModel} function are converted into | |||||
* error-bearing {@code Result} objects using the given {@code onError} | |||||
* function. | |||||
* | |||||
* @param <P> | |||||
* the presentation type | |||||
* @param <M> | |||||
* the model type | |||||
* @param toModel | |||||
* the function to convert to model | |||||
* @param toPresentation | |||||
* the function to convert to presentation | |||||
* @param onError | |||||
* the function to provide error messages | |||||
* @return the new converter | |||||
* | |||||
* @see Result | |||||
* @see Function | |||||
*/ | |||||
public static <P, M> Converter<P, M> from(Function<P, M> toModel, | |||||
Function<M, P> toPresentation, | |||||
Function<Exception, String> onError) { | |||||
return from(val -> Result.of(() -> toModel.apply(val), onError), | |||||
toPresentation); | |||||
} | |||||
/** | |||||
* Constructs a converter from a filter and a function. | |||||
* | |||||
* @param <P> | |||||
* the presentation type | |||||
* @param <M> | |||||
* the model type | |||||
* @param toModel | |||||
* the function to convert to model | |||||
* @param toPresentation | |||||
* the function to convert to presentation | |||||
* @return the new converter | |||||
* | |||||
* @see Function | |||||
*/ | |||||
public static <P, M> Converter<P, M> from(Function<P, Result<M>> toModel, | |||||
Function<M, P> toPresentation) { | |||||
return new Converter<P, M>() { | |||||
@Override | |||||
public Result<M> convertToModel(P value, Locale locale) | |||||
throws ConversionException { | |||||
return toModel.apply(value); | |||||
} | |||||
@Override | |||||
public P convertToPresentation(M value, Locale locale) | |||||
throws ConversionException { | |||||
return toPresentation.apply(value); | |||||
} | |||||
}; | |||||
} | |||||
/** | |||||
* Returns a converter that chains together this converter with the given | |||||
* type-compatible converter. | |||||
* <p> | |||||
* The chained converters will form a new converter capable of converting | |||||
* from the presentation type of this converter to the model type of the | |||||
* other converter. | |||||
* <p> | |||||
* In most typical cases you should not need this method but instead only | |||||
* need to define one converter for a binding using | |||||
* {@link Binding#withConverter(Converter)}. | |||||
* | |||||
* @param <T> | |||||
* the model type of the resulting converter | |||||
* @param other | |||||
* the converter to chain, not null | |||||
* @return a chained converter | |||||
*/ | |||||
public default <T> Converter<PRESENTATION, T> chain( | |||||
Converter<MODEL, T> other) { | |||||
return new Converter<PRESENTATION, T>() { | |||||
@Override | |||||
public Result<T> convertToModel(PRESENTATION value, Locale locale) { | |||||
Result<MODEL> model = Converter.this.convertToModel(value, | |||||
locale); | |||||
return model.flatMap(v -> other.convertToModel(v, locale)); | |||||
} | |||||
@Override | |||||
public PRESENTATION convertToPresentation(T value, Locale locale) { | |||||
MODEL model = other.convertToPresentation(value, locale); | |||||
return Converter.this.convertToPresentation(model, locale); | |||||
} | |||||
}; | |||||
} | |||||
} |
/* | |||||
* Copyright 2000-2014 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.util.converter; | |||||
import java.text.NumberFormat; | |||||
import java.util.Locale; | |||||
import com.vaadin.data.Result; | |||||
import com.vaadin.legacy.data.util.converter.LegacyConverter.ConversionException; | |||||
/** | |||||
* A converter that converts from {@link String} to {@link Integer} and back. | |||||
* Uses the given locale and a {@link NumberFormat} instance for formatting and | |||||
* parsing. | |||||
* <p> | |||||
* Override and overwrite {@link #getFormat(Locale)} to use a different format. | |||||
* </p> | |||||
* | |||||
* @author Vaadin Ltd | |||||
* @since 8.0 | |||||
*/ | |||||
public class StringToIntegerConverter | |||||
extends AbstractStringToNumberConverter<Integer> { | |||||
private final String errorMessage; | |||||
/** | |||||
* Creates a new converter instance with the given error message. | |||||
* | |||||
* @param errorMessage | |||||
* the error message to use if conversion fails | |||||
*/ | |||||
public StringToIntegerConverter(String errorMessage) { | |||||
this.errorMessage = errorMessage; | |||||
} | |||||
/** | |||||
* Returns the format used by | |||||
* {@link #convertToPresentation(Integer, Locale)} and | |||||
* {@link #convertToModel(String, Locale)}. | |||||
* | |||||
* @param locale | |||||
* The locale to use | |||||
* @return A NumberFormat instance | |||||
*/ | |||||
@Override | |||||
protected NumberFormat getFormat(Locale locale) { | |||||
if (locale == null) { | |||||
locale = Locale.getDefault(); | |||||
} | |||||
return NumberFormat.getIntegerInstance(locale); | |||||
} | |||||
@Override | |||||
public Result<Integer> convertToModel(String value, Locale locale) | |||||
throws ConversionException { | |||||
Number n = convertToNumber(value, locale); | |||||
if (n == null) { | |||||
return null; | |||||
} | |||||
int intValue = n.intValue(); | |||||
if (intValue == n.longValue()) { | |||||
// If the value of n is outside the range of long, the return value | |||||
// of longValue() is either Long.MIN_VALUE or Long.MAX_VALUE. The | |||||
// above comparison promotes int to long and thus does not need to | |||||
// consider wrap-around. | |||||
return Result.ok(intValue); | |||||
} else { | |||||
return Result.error(errorMessage); | |||||
} | |||||
} | |||||
} |
import org.junit.Before; | import org.junit.Before; | ||||
import org.junit.Test; | import org.junit.Test; | ||||
import com.vaadin.data.Binder; | |||||
import com.vaadin.data.ValidationError; | |||||
import com.vaadin.data.Binder.Binding; | |||||
import com.vaadin.data.util.converter.StringToIntegerConverter; | |||||
import com.vaadin.data.validator.EmailValidator; | import com.vaadin.data.validator.EmailValidator; | ||||
import com.vaadin.server.AbstractErrorMessage; | import com.vaadin.server.AbstractErrorMessage; | ||||
import com.vaadin.tests.data.bean.Person; | |||||
import com.vaadin.ui.AbstractField; | import com.vaadin.ui.AbstractField; | ||||
import com.vaadin.ui.Slider; | |||||
/** | /** | ||||
* Book of Vaadin tests. | * Book of Vaadin tests. | ||||
* | |||||
* | |||||
* @author Vaadin Ltd | * @author Vaadin Ltd | ||||
* | * | ||||
*/ | */ | ||||
} | } | ||||
} | } | ||||
private Binder<Person> binder; | |||||
private static class BookPerson { | |||||
private String lastName; | |||||
private String email; | |||||
private int yearOfBirth, salaryLevel; | |||||
private TextField field; | |||||
public String getLastName() { | |||||
return lastName; | |||||
} | |||||
public void setLastName(String lastName) { | |||||
this.lastName = lastName; | |||||
} | |||||
public BookPerson(int yearOfBirth, int salaryLevel) { | |||||
this.yearOfBirth = yearOfBirth; | |||||
this.salaryLevel = salaryLevel; | |||||
} | |||||
public int getYearOfBirth() { | |||||
return yearOfBirth; | |||||
} | |||||
public void setYearOfBirth(int yearOfBirth) { | |||||
this.yearOfBirth = yearOfBirth; | |||||
} | |||||
public int getSalaryLevel() { | |||||
return salaryLevel; | |||||
} | |||||
public void setSalaryLevel(int salaryLevel) { | |||||
this.salaryLevel = salaryLevel; | |||||
} | |||||
public String getEmail() { | |||||
return email; | |||||
} | |||||
public void setEmail(String email) { | |||||
this.email = email; | |||||
} | |||||
} | |||||
private Binder<BookPerson> binder; | |||||
private Person person = new Person(); | |||||
private TextField field; | |||||
@Before | @Before | ||||
public void setUp() { | public void setUp() { | ||||
// Explicit validator instance | // Explicit validator instance | ||||
.withValidator(new EmailValidator( | .withValidator(new EmailValidator( | ||||
"This doesn't look like a valid email address")) | "This doesn't look like a valid email address")) | ||||
.bind(Person::getEmail, Person::setEmail); | |||||
.bind(BookPerson::getEmail, BookPerson::setEmail); | |||||
field.setValue("not-email"); | field.setValue("not-email"); | ||||
List<ValidationError<?>> errors = binder.validate(); | List<ValidationError<?>> errors = binder.validate(); | ||||
// Validator defined based on a lambda and an error message | // Validator defined based on a lambda and an error message | ||||
.withValidator(name -> name.length() >= 3, | .withValidator(name -> name.length() >= 3, | ||||
"Last name must contain at least three characters") | "Last name must contain at least three characters") | ||||
.bind(Person::getLastName, Person::setLastName); | |||||
.bind(BookPerson::getLastName, BookPerson::setLastName); | |||||
field.setValue("a"); | field.setValue("a"); | ||||
List<ValidationError<?>> errors = binder.validate(); | List<ValidationError<?>> errors = binder.validate(); | ||||
"This doesn't look like a valid email address")) | "This doesn't look like a valid email address")) | ||||
.withValidator(email -> email.endsWith("@acme.com"), | .withValidator(email -> email.endsWith("@acme.com"), | ||||
"Only acme.com email addresses are allowed") | "Only acme.com email addresses are allowed") | ||||
.bind(Person::getEmail, Person::setEmail); | |||||
.bind(BookPerson::getEmail, BookPerson::setEmail); | |||||
field.setValue("not-email"); | field.setValue("not-email"); | ||||
List<ValidationError<?>> errors = binder.validate(); | List<ValidationError<?>> errors = binder.validate(); | ||||
Assert.assertEquals(2, errors.size()); | |||||
// Only one error per field should be reported | |||||
Assert.assertEquals(1, errors.size()); | |||||
Assert.assertEquals("This doesn't look like a valid email address", | Assert.assertEquals("This doesn't look like a valid email address", | ||||
errors.get(0).getMessage()); | errors.get(0).getMessage()); | ||||
Assert.assertEquals("Only acme.com email addresses are allowed", | |||||
errors.get(1).getMessage()); | |||||
Assert.assertEquals("This doesn't look like a valid email address", | Assert.assertEquals("This doesn't look like a valid email address", | ||||
((AbstractErrorMessage) field.getErrorMessage()).getMessage()); | ((AbstractErrorMessage) field.getErrorMessage()).getMessage()); | ||||
Assert.assertEquals(0, errors.size()); | Assert.assertEquals(0, errors.size()); | ||||
Assert.assertNull(field.getErrorMessage()); | Assert.assertNull(field.getErrorMessage()); | ||||
} | } | ||||
@Test | |||||
public void converterBookOfVaadinExample1() { | |||||
TextField yearOfBirthField = new TextField(); | |||||
// Slider for integers between 1 and 10 | |||||
Slider salaryLevelField = new Slider("Salary level", 1, 10); | |||||
Binding<BookPerson, String, String> b1 = binder | |||||
.forField(yearOfBirthField); | |||||
Binding<BookPerson, String, Integer> b2 = b1.withConverter( | |||||
new StringToIntegerConverter("Must enter a number")); | |||||
b2.bind(BookPerson::getYearOfBirth, BookPerson::setYearOfBirth); | |||||
Binding<BookPerson, Double, Double> salaryBinding1 = binder | |||||
.forField(salaryLevelField); | |||||
Binding<BookPerson, Double, Integer> salaryBinding2 = salaryBinding1 | |||||
.withConverter(Double::intValue, Integer::doubleValue); | |||||
salaryBinding2.bind(BookPerson::getSalaryLevel, | |||||
BookPerson::setSalaryLevel); | |||||
// Test that the book code works | |||||
BookPerson bookPerson = new BookPerson(1972, 4); | |||||
binder.bind(bookPerson); | |||||
Assert.assertEquals(4.0, salaryLevelField.getValue().doubleValue(), 0); | |||||
Assert.assertEquals("1,972", yearOfBirthField.getValue()); | |||||
bookPerson.setSalaryLevel(8); | |||||
binder.load(bookPerson); | |||||
Assert.assertEquals(8.0, salaryLevelField.getValue().doubleValue(), 0); | |||||
bookPerson.setYearOfBirth(123); | |||||
binder.load(bookPerson); | |||||
Assert.assertEquals("123", yearOfBirthField.getValue()); | |||||
yearOfBirthField.setValue("2016"); | |||||
salaryLevelField.setValue(1.0); | |||||
Assert.assertEquals(2016, bookPerson.getYearOfBirth()); | |||||
Assert.assertEquals(1, bookPerson.getSalaryLevel()); | |||||
} | |||||
@Test | |||||
public void converterBookOfVaadinExample2() { | |||||
TextField yearOfBirthField = new TextField(); | |||||
binder.forField(yearOfBirthField) | |||||
.withConverter(Integer::valueOf, String::valueOf, | |||||
// Text to use instead of the NumberFormatException | |||||
// message | |||||
"Please enter a number") | |||||
.bind(BookPerson::getYearOfBirth, BookPerson::setYearOfBirth); | |||||
binder.bind(new BookPerson(1900, 5)); | |||||
yearOfBirthField.setValue("abc"); | |||||
binder.validate(); | |||||
Assert.assertEquals("Please enter a number", | |||||
yearOfBirthField.getComponentError().getFormattedHtmlMessage()); | |||||
} | |||||
} | } |
import org.junit.Test; | import org.junit.Test; | ||||
import com.vaadin.data.Binder.Binding; | import com.vaadin.data.Binder.Binding; | ||||
import com.vaadin.data.util.converter.Converter; | |||||
import com.vaadin.server.AbstractErrorMessage; | import com.vaadin.server.AbstractErrorMessage; | ||||
import com.vaadin.server.ErrorMessage; | import com.vaadin.server.ErrorMessage; | ||||
import com.vaadin.server.UserError; | import com.vaadin.server.UserError; | ||||
} | } | ||||
} | } | ||||
private static class StatusBean { | |||||
private String status; | |||||
public String getStatus() { | |||||
return status; | |||||
} | |||||
public void setStatus(String status) { | |||||
this.status = status; | |||||
} | |||||
} | |||||
Binder<Person> binder; | Binder<Person> binder; | ||||
TextField nameField; | TextField nameField; | ||||
TextField ageField; | |||||
Person p = new Person(); | Person p = new Person(); | ||||
Validator<String> notEmpty = Validator.from(val -> !val.isEmpty(), | |||||
"Value cannot be empty"); | |||||
Converter<String, Integer> stringToInteger = Converter.from( | |||||
Integer::valueOf, String::valueOf, e -> "Value must be a number"); | |||||
Validator<Integer> notNegative = Validator.from(x -> x >= 0, | |||||
"Value must be positive"); | |||||
@Before | @Before | ||||
public void setUp() { | public void setUp() { | ||||
binder = new Binder<>(); | binder = new Binder<>(); | ||||
p.setFirstName("Johannes"); | p.setFirstName("Johannes"); | ||||
p.setAge(32); | |||||
nameField = new TextField(); | nameField = new TextField(); | ||||
ageField = new TextField(); | |||||
} | } | ||||
@Test(expected = NullPointerException.class) | @Test(expected = NullPointerException.class) | ||||
List<ValidationError<?>> errors = binder.validate(); | List<ValidationError<?>> errors = binder.validate(); | ||||
Assert.assertEquals(2, errors.size()); | |||||
Assert.assertEquals(1, errors.size()); | |||||
Set<String> errorMessages = errors.stream() | Set<String> errorMessages = errors.stream() | ||||
.map(ValidationError::getMessage).collect(Collectors.toSet()); | .map(ValidationError::getMessage).collect(Collectors.toSet()); | ||||
Assert.assertTrue(errorMessages.contains(msg1)); | Assert.assertTrue(errorMessages.contains(msg1)); | ||||
Assert.assertTrue(errorMessages.contains(msg2)); | |||||
Set<?> fields = errors.stream().map(ValidationError::getField) | Set<?> fields = errors.stream().map(ValidationError::getField) | ||||
.collect(Collectors.toSet()); | .collect(Collectors.toSet()); | ||||
binder.bind(p); | binder.bind(p); | ||||
} | } | ||||
private void bindAgeWithValidatorConverterValidator() { | |||||
binder.forField(ageField).withValidator(notEmpty) | |||||
.withConverter(stringToInteger).withValidator(notNegative) | |||||
.bind(Person::getAge, Person::setAge); | |||||
binder.bind(p); | |||||
} | |||||
@Test | |||||
public void validatorForSuperTypeCanBeUsed() { | |||||
// Validates that a validator for a super type can be used, e.g. | |||||
// validator for Number can be used on a Double | |||||
TextField salaryField = new TextField(); | |||||
Binder<Person> binder = new Binder<>(); | |||||
Validator<Number> positiveNumberValidator = value -> { | |||||
if (value.doubleValue() >= 0) { | |||||
return Result.ok(value); | |||||
} else { | |||||
return Result.error("Number must be positive"); | |||||
} | |||||
}; | |||||
binder.forField(salaryField) | |||||
.withConverter(Double::valueOf, String::valueOf) | |||||
.withValidator(positiveNumberValidator) | |||||
.bind(Person::getSalaryDouble, Person::setSalaryDouble); | |||||
Person person = new Person(); | |||||
binder.bind(person); | |||||
salaryField.setValue("10"); | |||||
Assert.assertEquals(10, person.getSalaryDouble(), 0); | |||||
salaryField.setValue("-1"); // Does not pass validator | |||||
Assert.assertEquals(10, person.getSalaryDouble(), 0); | |||||
} | |||||
@Test | |||||
public void convertInitialValue() { | |||||
bindAgeWithValidatorConverterValidator(); | |||||
assertEquals("32", ageField.getValue()); | |||||
} | |||||
@Test | |||||
public void convertToModelValidAge() { | |||||
bindAgeWithValidatorConverterValidator(); | |||||
ageField.setValue("33"); | |||||
assertEquals(33, p.getAge()); | |||||
} | |||||
@Test | |||||
public void convertToModelNegativeAgeFailsOnFirstValidator() { | |||||
bindAgeWithValidatorConverterValidator(); | |||||
ageField.setValue(""); | |||||
assertEquals(32, p.getAge()); | |||||
assertValidationErrors(binder.validate(), "Value cannot be empty"); | |||||
} | |||||
private void assertValidationErrors( | |||||
List<ValidationError<?>> validationErrors, | |||||
String... errorMessages) { | |||||
Assert.assertEquals(errorMessages.length, validationErrors.size()); | |||||
for (int i = 0; i < errorMessages.length; i++) { | |||||
Assert.assertEquals(errorMessages[i], | |||||
validationErrors.get(i).getMessage()); | |||||
} | |||||
} | |||||
@Test | |||||
public void convertToModelConversionFails() { | |||||
bindAgeWithValidatorConverterValidator(); | |||||
ageField.setValue("abc"); | |||||
assertEquals(32, p.getAge()); | |||||
assertValidationErrors(binder.validate(), "Value must be a number"); | |||||
} | |||||
@Test | |||||
public void convertToModelNegativeAgeFailsOnIntegerValidator() { | |||||
bindAgeWithValidatorConverterValidator(); | |||||
ageField.setValue("-5"); | |||||
assertEquals(32, p.getAge()); | |||||
assertValidationErrors(binder.validate(), "Value must be positive"); | |||||
} | |||||
@Test | |||||
public void convertDataToField() { | |||||
bindAgeWithValidatorConverterValidator(); | |||||
binder.getBean().get().setAge(12); | |||||
binder.load(binder.getBean().get()); | |||||
Assert.assertEquals("12", ageField.getValue()); | |||||
} | |||||
@Test | |||||
public void convertNotValidatableDataToField() { | |||||
bindAgeWithValidatorConverterValidator(); | |||||
binder.getBean().get().setAge(-12); | |||||
binder.load(binder.getBean().get()); | |||||
Assert.assertEquals("-12", ageField.getValue()); | |||||
} | |||||
@Test(expected = IllegalArgumentException.class) | |||||
public void convertInvalidDataToField() { | |||||
TextField field = new TextField(); | |||||
StatusBean bean = new StatusBean(); | |||||
bean.setStatus("1"); | |||||
Binder<StatusBean> binder = new Binder<StatusBean>(); | |||||
Binding<StatusBean, String, String> binding = binder.forField(field) | |||||
.withConverter(presentation -> { | |||||
if (presentation.equals("OK")) { | |||||
return "1"; | |||||
} else if (presentation.equals("NOTOK")) { | |||||
return "2"; | |||||
} | |||||
throw new IllegalArgumentException( | |||||
"Value must be OK or NOTOK"); | |||||
}, model -> { | |||||
if (model.equals("1")) { | |||||
return "OK"; | |||||
} else if (model.equals("2")) { | |||||
return "NOTOK"; | |||||
} else { | |||||
throw new IllegalArgumentException( | |||||
"Value in model must be 1 or 2"); | |||||
} | |||||
}); | |||||
binding.bind(StatusBean::getStatus, StatusBean::setStatus); | |||||
binder.bind(bean); | |||||
bean.setStatus("3"); | |||||
binder.load(bean); | |||||
} | |||||
} | } |