Change-Id: Ibf1223d4842d72f0209231dfd70e1d6c4deb6d30tags/8.0.0.alpha1
@@ -236,7 +236,7 @@ binder.forField(yearOfBirthField) | |||
Slider salaryLevelField = new Slider("Salary level", 1, 10); | |||
binder.forField(salaryLevelField) | |||
.withConverter(Integer::doubleValue, Double::intValue) | |||
.withConverter(Double::intValue, Integer::doubleValue) | |||
.bind(Person::getSalaryLevel, Person::setSalaryLevel); | |||
---- |
@@ -19,15 +19,18 @@ import java.io.Serializable; | |||
import java.util.ArrayList; | |||
import java.util.LinkedHashSet; | |||
import java.util.List; | |||
import java.util.Locale; | |||
import java.util.Objects; | |||
import java.util.Optional; | |||
import java.util.Set; | |||
import java.util.function.BiConsumer; | |||
import java.util.function.Function; | |||
import java.util.function.Predicate; | |||
import java.util.stream.Collectors; | |||
import com.vaadin.data.util.converter.Converter; | |||
import com.vaadin.data.util.converter.StringToIntegerConverter; | |||
import com.vaadin.event.Registration; | |||
import com.vaadin.server.ErrorMessage; | |||
import com.vaadin.server.UserError; | |||
import com.vaadin.ui.AbstractComponent; | |||
@@ -153,6 +156,105 @@ public class Binder<BEAN> implements Serializable { | |||
public Binding<BEAN, FIELDVALUE, TARGET> withValidator( | |||
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. | |||
* | |||
@@ -176,15 +278,19 @@ public class Binder<BEAN> implements Serializable { | |||
protected static class BindingImpl<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 Function<BEAN, TARGET> getter; | |||
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. | |||
@@ -194,9 +300,28 @@ public class Binder<BEAN> implements Serializable { | |||
* @param field | |||
* the field to bind | |||
*/ | |||
@SuppressWarnings("unchecked") | |||
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.binder = binder; | |||
this.converterValidatorChain = converterValidatorChain; | |||
} | |||
@Override | |||
@@ -216,7 +341,11 @@ public class Binder<BEAN> implements Serializable { | |||
Validator<? super TARGET> validator) { | |||
checkUnbound(); | |||
Objects.requireNonNull(validator, "validator cannot be null"); | |||
validators.add(validator); | |||
Converter<TARGET, TARGET> validatorAsConverter = new ValidatorAsConverter<>( | |||
validator); | |||
converterValidatorChain = converterValidatorChain | |||
.chain(validatorAsConverter); | |||
return this; | |||
} | |||
@@ -226,20 +355,38 @@ public class Binder<BEAN> implements Serializable { | |||
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) { | |||
setFieldValue(bean); | |||
onValueChange = field | |||
.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() { | |||
@@ -255,7 +402,13 @@ public class Binder<BEAN> implements Serializable { | |||
*/ | |||
private void setFieldValue(BEAN bean) { | |||
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()); | |||
} | |||
/** | |||
@@ -268,12 +421,12 @@ public class Binder<BEAN> implements Serializable { | |||
private void storeFieldValue(BEAN bean) { | |||
assert bean != null; | |||
if (setter != null) { | |||
setter.accept(bean, (TARGET) field.getValue()); | |||
getTargetValue().ifPresent(value -> setter.accept(bean, value)); | |||
} | |||
} | |||
private void checkUnbound() { | |||
if (this.getter != null) { | |||
if (getter != null) { | |||
throw new IllegalStateException( | |||
"cannot modify binding: already bound to a property"); | |||
} | |||
@@ -285,6 +438,46 @@ public class Binder<BEAN> implements Serializable { | |||
} | |||
} | |||
/** | |||
* 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 Set<BindingImpl<BEAN, ?, ?>> bindings = new LinkedHashSet<>(); | |||
@@ -396,12 +589,13 @@ public class Binder<BEAN> implements Serializable { | |||
public List<ValidationError<?>> validate() { | |||
List<ValidationError<?>> resultErrors = new ArrayList<>(); | |||
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; | |||
} | |||
@@ -456,11 +650,11 @@ public class Binder<BEAN> implements Serializable { | |||
* the field to bind | |||
* @return the new incomplete binding | |||
*/ | |||
protected <FIELDVALUE> Binding<BEAN, FIELDVALUE, FIELDVALUE> createBinding( | |||
protected <FIELDVALUE> BindingImpl<BEAN, FIELDVALUE, FIELDVALUE> createBinding( | |||
HasValue<FIELDVALUE> field) { | |||
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; | |||
} | |||
@@ -1,12 +1,12 @@ | |||
/* | |||
* 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 | |||
@@ -40,7 +40,7 @@ public interface Result<R> extends Serializable { | |||
/** | |||
* Returns a successful result wrapping the given value. | |||
* | |||
* | |||
* @param <R> | |||
* the result value type | |||
* @param value | |||
@@ -53,7 +53,7 @@ public interface Result<R> extends Serializable { | |||
/** | |||
* Returns a failure result wrapping the given error message. | |||
* | |||
* | |||
* @param <R> | |||
* the result value type | |||
* @param message | |||
@@ -70,7 +70,7 @@ public interface Result<R> extends Serializable { | |||
* If the supplier returns a value, returns a {@code Result.ok} of the | |||
* value; if an exception is thrown, returns the message in a | |||
* {@code Result.error}. | |||
* | |||
* | |||
* @param <R> | |||
* the result value type | |||
* @param supplier | |||
@@ -96,7 +96,7 @@ public interface Result<R> extends Serializable { | |||
* 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 | |||
* not wrapped but allowed to propagate. | |||
* | |||
* | |||
* @param <S> | |||
* the type of the mapped value | |||
* @param mapper | |||
@@ -112,7 +112,7 @@ public interface Result<R> extends Serializable { | |||
* 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 | |||
* wrapped but allowed to propagate. | |||
* | |||
* | |||
* @param <S> | |||
* the type of the mapped value | |||
* @param mapper | |||
@@ -124,7 +124,7 @@ public interface Result<R> extends Serializable { | |||
/** | |||
* Invokes either the first callback or the second one, depending on whether | |||
* this Result denotes a success or a failure, respectively. | |||
* | |||
* | |||
* @param ifOk | |||
* the function to call if success | |||
* @param ifError | |||
@@ -134,7 +134,7 @@ public interface Result<R> extends Serializable { | |||
/** | |||
* Applies the {@code consumer} if result is not an error. | |||
* | |||
* | |||
* @param consumer | |||
* consumer to apply in case it's not an error | |||
*/ | |||
@@ -145,7 +145,7 @@ public interface Result<R> extends Serializable { | |||
/** | |||
* Applies the {@code consumer} if result is an error. | |||
* | |||
* | |||
* @param consumer | |||
* consumer to apply in case it's an error | |||
*/ | |||
@@ -156,15 +156,22 @@ public interface Result<R> extends Serializable { | |||
/** | |||
* Returns {@code true} if result is an error. | |||
* | |||
* | |||
* @return whether the result is an error | |||
*/ | |||
public boolean isError(); | |||
/** | |||
* Returns an Optional of the result message, or an empty Optional if none. | |||
* | |||
* | |||
* @return the optional message | |||
*/ | |||
public Optional<String> getMessage(); | |||
/** | |||
* Returns an Optional of the value, or an empty Optional if none. | |||
* | |||
* @return the optional value | |||
*/ | |||
public Optional<R> getValue(); | |||
} |
@@ -37,7 +37,7 @@ class SimpleResult<R> implements Result<R> { | |||
* <p> | |||
* If {@code message} is null then {@code value} is ignored and result is an | |||
* error. | |||
* | |||
* | |||
* @param value | |||
* the value of the result, may be {@code null} | |||
* @param message | |||
@@ -80,6 +80,11 @@ class SimpleResult<R> implements Result<R> { | |||
return Optional.ofNullable(message); | |||
} | |||
@Override | |||
public Optional<R> getValue() { | |||
return Optional.ofNullable(value); | |||
} | |||
@Override | |||
public boolean isError() { | |||
return message != null; | |||
@@ -93,4 +98,5 @@ class SimpleResult<R> implements Result<R> { | |||
return "ok(" + value + ")"; | |||
} | |||
} | |||
} |
@@ -0,0 +1,102 @@ | |||
/* | |||
* 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); | |||
} | |||
} |
@@ -0,0 +1,176 @@ | |||
/* | |||
* 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); | |||
} | |||
}; | |||
} | |||
} |
@@ -0,0 +1,89 @@ | |||
/* | |||
* 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); | |||
} | |||
} | |||
} |
@@ -21,16 +21,16 @@ import org.junit.Assert; | |||
import org.junit.Before; | |||
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.server.AbstractErrorMessage; | |||
import com.vaadin.tests.data.bean.Person; | |||
import com.vaadin.ui.AbstractField; | |||
import com.vaadin.ui.Slider; | |||
/** | |||
* Book of Vaadin tests. | |||
* | |||
* | |||
* @author Vaadin Ltd | |||
* | |||
*/ | |||
@@ -51,11 +51,53 @@ public class BinderBookOfVaadinTest { | |||
} | |||
} | |||
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 | |||
public void setUp() { | |||
@@ -69,7 +111,7 @@ public class BinderBookOfVaadinTest { | |||
// Explicit validator instance | |||
.withValidator(new EmailValidator( | |||
"This doesn't look like a valid email address")) | |||
.bind(Person::getEmail, Person::setEmail); | |||
.bind(BookPerson::getEmail, BookPerson::setEmail); | |||
field.setValue("not-email"); | |||
List<ValidationError<?>> errors = binder.validate(); | |||
@@ -91,7 +133,7 @@ public class BinderBookOfVaadinTest { | |||
// Validator defined based on a lambda and an error message | |||
.withValidator(name -> name.length() >= 3, | |||
"Last name must contain at least three characters") | |||
.bind(Person::getLastName, Person::setLastName); | |||
.bind(BookPerson::getLastName, BookPerson::setLastName); | |||
field.setValue("a"); | |||
List<ValidationError<?>> errors = binder.validate(); | |||
@@ -115,15 +157,14 @@ public class BinderBookOfVaadinTest { | |||
"This doesn't look like a valid email address")) | |||
.withValidator(email -> email.endsWith("@acme.com"), | |||
"Only acme.com email addresses are allowed") | |||
.bind(Person::getEmail, Person::setEmail); | |||
.bind(BookPerson::getEmail, BookPerson::setEmail); | |||
field.setValue("not-email"); | |||
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", | |||
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", | |||
((AbstractErrorMessage) field.getErrorMessage()).getMessage()); | |||
@@ -140,4 +181,61 @@ public class BinderBookOfVaadinTest { | |||
Assert.assertEquals(0, errors.size()); | |||
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()); | |||
} | |||
} |
@@ -13,6 +13,7 @@ import org.junit.Before; | |||
import org.junit.Test; | |||
import com.vaadin.data.Binder.Binding; | |||
import com.vaadin.data.util.converter.Converter; | |||
import com.vaadin.server.AbstractErrorMessage; | |||
import com.vaadin.server.ErrorMessage; | |||
import com.vaadin.server.UserError; | |||
@@ -36,17 +37,40 @@ public class BinderTest { | |||
} | |||
} | |||
private static class StatusBean { | |||
private String status; | |||
public String getStatus() { | |||
return status; | |||
} | |||
public void setStatus(String status) { | |||
this.status = status; | |||
} | |||
} | |||
Binder<Person> binder; | |||
TextField nameField; | |||
TextField ageField; | |||
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 | |||
public void setUp() { | |||
binder = new Binder<>(); | |||
p.setFirstName("Johannes"); | |||
p.setAge(32); | |||
nameField = new TextField(); | |||
ageField = new TextField(); | |||
} | |||
@Test(expected = NullPointerException.class) | |||
@@ -239,12 +263,11 @@ public class BinderTest { | |||
List<ValidationError<?>> errors = binder.validate(); | |||
Assert.assertEquals(2, errors.size()); | |||
Assert.assertEquals(1, errors.size()); | |||
Set<String> errorMessages = errors.stream() | |||
.map(ValidationError::getMessage).collect(Collectors.toSet()); | |||
Assert.assertTrue(errorMessages.contains(msg1)); | |||
Assert.assertTrue(errorMessages.contains(msg2)); | |||
Set<?> fields = errors.stream().map(ValidationError::getField) | |||
.collect(Collectors.toSet()); | |||
@@ -262,4 +285,137 @@ public class BinderTest { | |||
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); | |||
} | |||
} |