Browse Source

Add converter support to Binder

Change-Id: Ibf1223d4842d72f0209231dfd70e1d6c4deb6d30
tags/8.0.0.alpha1
Artur Signell 7 years ago
parent
commit
f2bb3c886c

+ 1
- 1
documentation/datamodel/datamodel-forms.asciidoc View File

@@ -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);

----

+ 220
- 26
server/src/main/java/com/vaadin/data/Binder.java View File

@@ -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;
}


+ 20
- 13
server/src/main/java/com/vaadin/data/Result.java View File

@@ -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();
}

+ 7
- 1
server/src/main/java/com/vaadin/data/SimpleResult.java View File

@@ -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 + ")";
}
}

}

+ 102
- 0
server/src/main/java/com/vaadin/data/util/converter/AbstractStringToNumberConverter.java View File

@@ -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);
}

}

+ 176
- 0
server/src/main/java/com/vaadin/data/util/converter/Converter.java View File

@@ -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);
}
};
}
}

+ 89
- 0
server/src/main/java/com/vaadin/data/util/converter/StringToIntegerConverter.java View File

@@ -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);
}
}

}

+ 111
- 13
server/src/test/java/com/vaadin/data/BinderBookOfVaadinTest.java View File

@@ -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&#32;enter&#32;a&#32;number",
yearOfBirthField.getComponentError().getFormattedHtmlMessage());
}

}

+ 158
- 2
server/src/test/java/com/vaadin/data/BinderTest.java View File

@@ -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);
}

}

Loading…
Cancel
Save