diff options
author | Johannes Dahlström <johannesd@vaadin.com> | 2016-07-25 12:49:26 +0300 |
---|---|---|
committer | Vaadin Code Review <review@vaadin.com> | 2016-08-22 12:17:13 +0000 |
commit | ec8904f6b0ab77231d567daa35c9cc7138b6fe59 (patch) | |
tree | 7985a8d935fd5f326bd8aad50e827bf5cf747cdb | |
parent | 762ca747ce33f385e65fd9d77e9f102c3b86ac05 (diff) | |
download | vaadin-framework-ec8904f6b0ab77231d567daa35c9cc7138b6fe59.tar.gz vaadin-framework-ec8904f6b0ab77231d567daa35c9cc7138b6fe59.zip |
Implement BeanBinder with JSR-303 validation
Change-Id: Ieaba56e9a26381d98b139845c30d65340dac0639
8 files changed, 692 insertions, 121 deletions
diff --git a/compatibility-server/src/main/java/com/vaadin/data/util/BeanItem.java b/compatibility-server/src/main/java/com/vaadin/data/util/BeanItem.java index c0c4fdcaa6..2725606a4d 100644 --- a/compatibility-server/src/main/java/com/vaadin/data/util/BeanItem.java +++ b/compatibility-server/src/main/java/com/vaadin/data/util/BeanItem.java @@ -188,7 +188,7 @@ public class BeanItem<BT> extends PropertysetItem { // Try to introspect, if it fails, we just have an empty Item try { List<PropertyDescriptor> propertyDescriptors = BeanUtil - .getBeanPropertyDescriptor(beanClass); + .getBeanPropertyDescriptors(beanClass); // Add all the bean properties as MethodProperties to this Item // later entries on the list overwrite earlier ones diff --git a/server/src/main/java/com/vaadin/data/BeanBinder.java b/server/src/main/java/com/vaadin/data/BeanBinder.java new file mode 100644 index 0000000000..214cb6781f --- /dev/null +++ b/server/src/main/java/com/vaadin/data/BeanBinder.java @@ -0,0 +1,322 @@ +/* + * Copyright 2000-2016 Vaadin Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ + +package com.vaadin.data; + +import java.beans.IntrospectionException; +import java.beans.PropertyDescriptor; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.util.Locale; +import java.util.Objects; +import java.util.function.Function; +import java.util.function.Predicate; + +import com.vaadin.data.util.BeanUtil; +import com.vaadin.data.util.converter.Converter; +import com.vaadin.data.validator.BeanValidator; +import com.vaadin.ui.Component; +import com.vaadin.ui.UI; + +/** + * A {@code Binder} subclass specialized for binding <em>beans</em>: classes + * that conform to the JavaBeans specification. Bean properties are bound by + * their names. If a JSR-303 bean validation implementation is present on the + * classpath, {@code BeanBinder} adds a {@link BeanValidator} to each binding. + * + * @author Vaadin Ltd. + * + * @param <BEAN> + * the bean type + * + * @since + */ +public class BeanBinder<BEAN> extends Binder<BEAN> { + + /** + * Represents the binding between a single field and a bean property. + * + * @param <BEAN> + * the bean type + * @param <FIELDVALUE> + * the field value type + * @param <TARGET> + * the target property type + */ + public interface BeanBinding<BEAN, FIELDVALUE, TARGET> extends + Binding<BEAN, FIELDVALUE, TARGET> { + + @Override + public BeanBinding<BEAN, FIELDVALUE, TARGET> withValidator( + Validator<? super TARGET> validator); + + @Override + public default BeanBinding<BEAN, FIELDVALUE, TARGET> withValidator( + Predicate<? super TARGET> predicate, String message) { + return (BeanBinding<BEAN, FIELDVALUE, TARGET>) Binding.super.withValidator( + predicate, message); + } + + @Override + public <NEWTARGET> BeanBinding<BEAN, FIELDVALUE, NEWTARGET> withConverter( + Converter<TARGET, NEWTARGET> converter); + + @Override + public default <NEWTARGET> BeanBinding<BEAN, FIELDVALUE, NEWTARGET> withConverter( + Function<TARGET, NEWTARGET> toModel, + Function<NEWTARGET, TARGET> toPresentation) { + return (BeanBinding<BEAN, FIELDVALUE, NEWTARGET>) Binding.super.withConverter( + toModel, toPresentation); + } + + @Override + public default <NEWTARGET> BeanBinding<BEAN, FIELDVALUE, NEWTARGET> withConverter( + Function<TARGET, NEWTARGET> toModel, + Function<NEWTARGET, TARGET> toPresentation, + String errorMessage) { + return (BeanBinding<BEAN, FIELDVALUE, NEWTARGET>) Binding.super.withConverter( + toModel, toPresentation, errorMessage); + } + + /** + * Completes this binding by connecting the field to the property with + * the given name. The getter and setter methods of the property are + * looked up with bean introspection and used to read and write the + * property value. + * <p> + * If a JSR-303 bean validation implementation is present on the + * classpath, adds a {@link BeanValidator} to this binding. + * <p> + * The property must have an accessible getter method. It need not have + * an accessible setter; in that case the property value is never + * updated and the binding is said to be <i>read-only</i>. + * + * @param propertyName + * the name of the property to bind, not null + * + * @throws IllegalArgumentException + * if the property name is invalid + * @throws IllegalArgumentException + * if the property has no accessible getter + * + * @see Binding#bind(Function, java.util.function.BiConsumer) + */ + public void bind(String propertyName); + } + + /** + * An internal implementation of {@link BeanBinding}. + * + * @param <BEAN> + * the bean type + * @param <FIELDVALUE> + * the field value type + * @param <TARGET> + * the target property type + */ + protected static class BeanBindingImpl<BEAN, FIELDVALUE, TARGET> extends + BindingImpl<BEAN, FIELDVALUE, TARGET> + implements BeanBinding<BEAN, FIELDVALUE, TARGET> { + + private Method getter; + private Method setter; + + /** + * Creates a new bean binding. + * + * @param binder + * the binder this instance is connected to, not null + * @param field + * the field to use, not null + * @param converter + * the initial converter to use, not null + * @param statusChangeHandler + * the handler to notify of status changes, not null + */ + protected BeanBindingImpl(BeanBinder<BEAN> binder, + HasValue<FIELDVALUE> field, + Converter<FIELDVALUE, TARGET> converter, + StatusChangeHandler statusChangeHandler) { + super(binder, field, converter, statusChangeHandler); + } + + @Override + public BeanBinding<BEAN, FIELDVALUE, TARGET> withValidator( + Validator<? super TARGET> validator) { + return (BeanBinding<BEAN, FIELDVALUE, TARGET>) super.withValidator( + validator); + } + + @Override + public <NEWTARGET> BeanBinding<BEAN, FIELDVALUE, NEWTARGET> withConverter( + Converter<TARGET, NEWTARGET> converter) { + return (BeanBinding<BEAN, FIELDVALUE, NEWTARGET>) super.withConverter( + converter); + } + + @Override + public void bind(String propertyName) { + checkUnbound(); + + Binding<BEAN, FIELDVALUE, Object> finalBinding; + + finalBinding = withConverter(createConverter()); + + if (BeanValidator.checkBeanValidationAvailable()) { + finalBinding = finalBinding.withValidator(new BeanValidator( + getBinder().beanType, propertyName, getLocale())); + } + + PropertyDescriptor descriptor = getDescriptor(propertyName); + getter = descriptor.getReadMethod(); + setter = descriptor.getWriteMethod(); + finalBinding.bind(this::getValue, this::setValue); + } + + @Override + protected BeanBinder<BEAN> getBinder() { + return (BeanBinder<BEAN>) super.getBinder(); + } + + private void setValue(BEAN bean, Object value) { + try { + if (setter != null) { + setter.invoke(bean, value); + } + } catch (IllegalAccessException | InvocationTargetException e) { + throw new RuntimeException(e); + } + } + + private Object getValue(BEAN bean) { + try { + return getter.invoke(bean); + } catch (IllegalAccessException | InvocationTargetException e) { + throw new RuntimeException(e); + } + } + + private PropertyDescriptor getDescriptor(String propertyName) { + final Class<?> beanType = getBinder().beanType; + PropertyDescriptor descriptor = null; + try { + descriptor = BeanUtil.getPropertyDescriptor(beanType, + propertyName); + } catch (IntrospectionException ie) { + throw new IllegalArgumentException( + "Could not resolve bean property name (see the cause): " + + beanType.getName() + "." + propertyName, ie); + } + if (descriptor == null) { + throw new IllegalArgumentException( + "Could not resolve bean property name (please check spelling and getter visibility): " + + beanType.getName() + "." + propertyName); + } + if (descriptor.getReadMethod() == null) { + throw new IllegalArgumentException( + "Bean property has no accessible getter: " + + beanType.getName() + "." + propertyName); + } + return descriptor; + } + + @SuppressWarnings("unchecked") + private Converter<TARGET, Object> createConverter() { + return Converter.from( + fieldValue -> getter.getReturnType().cast(fieldValue), + propertyValue -> (TARGET) propertyValue, + exception -> { + throw new RuntimeException(exception); + }); + } + + private Locale getLocale() { + Locale l = null; + if (getField() instanceof Component) { + l = ((Component) getField()).getLocale(); + } + if (l == null && UI.getCurrent() != null) { + l = UI.getCurrent().getLocale(); + } + if (l == null) { + l = Locale.getDefault(); + } + return l; + } + } + + private final Class<? extends BEAN> beanType; + + /** + * Creates a new {@code BeanBinder} supporting beans of the given type. + * + * @param beanType + * the bean {@code Class} instance, not null + */ + public BeanBinder(Class<? extends BEAN> beanType) { + BeanValidator.checkBeanValidationAvailable(); + this.beanType = beanType; + } + + @Override + public <FIELDVALUE> BeanBinding<BEAN, FIELDVALUE, FIELDVALUE> forField( + HasValue<FIELDVALUE> field) { + return createBinding(field, Converter.identity(), + this::handleValidationStatusChange); + } + + /** + * Binds the given field to the property with the given name. The getter and + * setter methods of the property are looked up with bean introspection and + * used to read and write the property value. + * <p> + * Use the {@link #forField(HasValue)} overload instead if you want to + * further configure the new binding. + * <p> + * The property must have an accessible getter method. It need not have an + * accessible setter; in that case the property value is never updated and + * the binding is said to be <i>read-only</i>. + * + * @param <FIELDVALUE> + * the value type of the field to bind + * @param field + * the field to bind, not null + * @param propertyName + * the name of the property to bind, not null + * + * @throws IllegalArgumentException + * if the property name is invalid + * @throws IllegalArgumentException + * if the property has no accessible getter + * + * @see #bind(HasValue, java.util.function.Function, + * java.util.function.BiConsumer) + */ + public <FIELDVALUE> void bind(HasValue<FIELDVALUE> field, + String propertyName) { + forField(field).bind(propertyName); + } + + @Override + protected <FIELDVALUE, TARGET> BeanBindingImpl<BEAN, FIELDVALUE, TARGET> createBinding( + HasValue<FIELDVALUE> field, + Converter<FIELDVALUE, TARGET> converter, + StatusChangeHandler handler) { + Objects.requireNonNull(field, "field cannot be null"); + Objects.requireNonNull(converter, "converter cannot be null"); + return new BeanBindingImpl<>(this, field, converter, handler); + } +} diff --git a/server/src/main/java/com/vaadin/data/Binder.java b/server/src/main/java/com/vaadin/data/Binder.java index d4501955d6..c7c55bfe7e 100644 --- a/server/src/main/java/com/vaadin/data/Binder.java +++ b/server/src/main/java/com/vaadin/data/Binder.java @@ -154,8 +154,10 @@ public class Binder<BEAN> implements Serializable { * @throws IllegalStateException * if {@code bind} has already been called */ - public Binding<BEAN, FIELDVALUE, TARGET> withValidator( - Predicate<? super TARGET> predicate, String message); + public default Binding<BEAN, FIELDVALUE, TARGET> withValidator( + Predicate<? super TARGET> predicate, String message) { + return withValidator(Validator.from(predicate, message)); + } /** * Maps the binding to another data type using the given @@ -210,7 +212,7 @@ public class Binder<BEAN> implements Serializable { * @throws IllegalStateException * if {@code bind} has already been called */ - default public <NEWTARGET> Binding<BEAN, FIELDVALUE, NEWTARGET> withConverter( + public default <NEWTARGET> Binding<BEAN, FIELDVALUE, NEWTARGET> withConverter( Function<TARGET, NEWTARGET> toModel, Function<NEWTARGET, TARGET> toPresentation) { return withConverter(Converter.from(toModel, toPresentation, @@ -381,35 +383,17 @@ public class Binder<BEAN> implements Serializable { private Converter<FIELDVALUE, TARGET> converterValidatorChain; /** - * Creates a new binding associated with the given field. - * - * @param binder - * the binder this instance is connected to - * @param field - * the field to bind - * @param statusChangeHandler - * handler to track validation status - */ - @SuppressWarnings("unchecked") - protected BindingImpl(Binder<BEAN> binder, HasValue<FIELDVALUE> field, - StatusChangeHandler statusChangeHandler) { - this(binder, field, - (Converter<FIELDVALUE, TARGET>) Converter.identity(), - statusChangeHandler); - } - - /** - * Creates a new binding associated with the given field using the given - * converter chain. + * Creates a new binding associated with the given field. Initializes + * the binding with the given converter chain and status change handler. * * @param binder - * the binder this instance is connected to + * the binder this instance is connected to, not null * @param field - * the field to bind + * the field to bind, not null * @param converterValidatorChain - * the converter/validator chain to use + * the converter/validator chain to use, not null * @param statusChangeHandler - * handler to track validation status + * the handler to track validation status, not null */ protected BindingImpl(Binder<BEAN> binder, HasValue<FIELDVALUE> field, Converter<FIELDVALUE, TARGET> converterValidatorChain, @@ -428,8 +412,8 @@ public class Binder<BEAN> implements Serializable { this.getter = getter; this.setter = setter; - binder.bindings.add(this); - binder.getBean().ifPresent(this::bind); + getBinder().bindings.add(this); + getBinder().getBean().ifPresent(this::bind); } @Override @@ -438,46 +422,68 @@ public class Binder<BEAN> implements Serializable { checkUnbound(); Objects.requireNonNull(validator, "validator cannot be null"); - Converter<TARGET, TARGET> validatorAsConverter = new ValidatorAsConverter<>( - validator); converterValidatorChain = converterValidatorChain - .chain(validatorAsConverter); + .chain(new ValidatorAsConverter<>(validator)); return this; } @Override - public Binding<BEAN, FIELDVALUE, TARGET> withValidator( - Predicate<? super TARGET> predicate, String 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"); - return createNewBinding(converter); + return getBinder().createBinding(getField(), converterValidatorChain + .chain(converter), statusChangeHandler); } @Override public Binding<BEAN, FIELDVALUE, TARGET> withStatusChangeHandler( StatusChangeHandler handler) { - Objects.requireNonNull(handler, "Handler may not be null"); + checkUnbound(); + Objects.requireNonNull(handler, "handler cannot be null"); if (isStatusHandlerChanged) { throw new IllegalStateException( "A StatusChangeHandler has already been set"); } isStatusHandlerChanged = true; - checkUnbound(); statusChangeHandler = handler; return this; } + @Override + public HasValue<FIELDVALUE> getField() { + return field; + } + + /** + * Returns the {@code Binder} connected to this {@code Binding} + * instance. + * + * @return the binder + */ + protected Binder<BEAN> getBinder() { + return binder; + } + + /** + * Throws if this binding is already completed and cannot be modified + * anymore. + * + * @throws IllegalStateException + * if this binding is already bound + */ + protected void checkUnbound() { + if (getter != null) { + throw new IllegalStateException( + "cannot modify binding: already bound to a property"); + } + } + private void bind(BEAN bean) { setFieldValue(bean); - onValueChange = field - .addValueChangeListener(e -> storeFieldValue(bean)); + onValueChange = getField().addValueChangeListener( + e -> storeFieldValue(bean)); } @Override @@ -512,13 +518,13 @@ public class Binder<BEAN> implements Serializable { */ private void setFieldValue(BEAN bean) { assert bean != null; - field.setValue(convertDataToFieldType(bean)); + getField().setValue(convertDataToFieldType(bean)); } private FIELDVALUE convertDataToFieldType(BEAN bean) { return converterValidatorChain.convertToPresentation( - getter.apply(bean), - ((AbstractComponent) field).getLocale()); + getter.apply(bean), ((AbstractComponent) getField()) + .getLocale()); } /** @@ -535,26 +541,6 @@ public class Binder<BEAN> implements Serializable { } } - private void checkUnbound() { - if (getter != null) { - throw new IllegalStateException( - "cannot modify binding: already bound to a property"); - } - } - - @Override - public HasValue<FIELDVALUE> getField() { - return field; - } - - private <NEWTARGET> BindingImpl<BEAN, FIELDVALUE, NEWTARGET> createNewBinding( - Converter<TARGET, NEWTARGET> converter) { - BindingImpl<BEAN, FIELDVALUE, NEWTARGET> newBinding = new BindingImpl<>( - binder, field, converterValidatorChain.chain(converter), - statusChangeHandler); - return newBinding; - } - private void fireStatusChangeEvent(Result<TARGET> result) { ValidationStatusChangeEvent event = new ValidationStatusChangeEvent( getField(), @@ -634,7 +620,9 @@ public class Binder<BEAN> implements Serializable { */ public <FIELDVALUE> Binding<BEAN, FIELDVALUE, FIELDVALUE> forField( HasValue<FIELDVALUE> field) { - return createBinding(field); + Objects.requireNonNull(field, "field cannot be null"); + return createBinding(field, Converter.identity(), + this::handleValidationStatusChange); } /** @@ -714,11 +702,12 @@ public class Binder<BEAN> implements Serializable { * @return the validation result. */ public List<ValidationError<?>> validate() { + List<ValidationError<?>> resultErrors = new ArrayList<>(); - for (BindingImpl<BEAN, ?, ?> binding : bindings) { - Result<?> result = binding.validate(); - result.ifError(errorMessage -> resultErrors - .add(new ValidationError<>(binding.field, errorMessage))); + for (BindingImpl<?, ?, ?> binding : bindings) { + binding.validate().ifError(errorMessage -> resultErrors + .add(new ValidationError<>(binding.getField(), + errorMessage))); } return resultErrors; } @@ -768,16 +757,22 @@ public class Binder<BEAN> implements Serializable { * * @param <FIELDVALUE> * the value type of the field + * @param <TARGET> + * the target data type * @param field - * the field to bind + * the field to bind, not null + * @param converter + * the converter for converting between FIELDVALUE and TARGET + * types, not null + * @param handler + * the handler to notify of status changes, not null * @return the new incomplete binding */ - protected <FIELDVALUE> Binding<BEAN, FIELDVALUE, FIELDVALUE> createBinding( - HasValue<FIELDVALUE> field) { - Objects.requireNonNull(field, "field cannot be null"); - BindingImpl<BEAN, FIELDVALUE, FIELDVALUE> b = new BindingImpl<BEAN, FIELDVALUE, FIELDVALUE>( - this, field, this::handleValidationStatusChange); - return b; + protected <FIELDVALUE, TARGET> BindingImpl<BEAN, FIELDVALUE, TARGET> createBinding( + HasValue<FIELDVALUE> field, + Converter<FIELDVALUE, TARGET> converter, + StatusChangeHandler handler) { + return new BindingImpl<>(this, field, converter, handler); } /** @@ -819,7 +814,7 @@ public class Binder<BEAN> implements Serializable { * @param event * the validation event */ - private void handleValidationStatusChange( + protected void handleValidationStatusChange( ValidationStatusChangeEvent event) { HasValue<?> source = event.getSource(); clearError(source); diff --git a/server/src/main/java/com/vaadin/data/util/BeanUtil.java b/server/src/main/java/com/vaadin/data/util/BeanUtil.java index ca897d57c5..375c449701 100644 --- a/server/src/main/java/com/vaadin/data/util/BeanUtil.java +++ b/server/src/main/java/com/vaadin/data/util/BeanUtil.java @@ -44,70 +44,102 @@ public final class BeanUtil implements Serializable { * both the setter and the getter for a property must be in the same * interface and should not be overridden in subinterfaces for the discovery * to work correctly. - * + * <p> * NOTE : This utility method relies on introspection (and returns * PropertyDescriptor) which is a part of java.beans package. The latter * package could require bigger JDK in the future (with Java 9+). So it may * be changed in the future. - * + * <p> * For interfaces, the iteration is depth first and the properties of * superinterfaces are returned before those of their subinterfaces. - * - * @param beanClass - * @return + * + * @param beanType + * the type whose properties to query + * @return a list of property descriptors of the given type * @throws IntrospectionException + * if the introspection fails */ - public static List<PropertyDescriptor> getBeanPropertyDescriptor( - final Class<?> beanClass) throws IntrospectionException { + public static List<PropertyDescriptor> getBeanPropertyDescriptors( + final Class<?> beanType) throws IntrospectionException { // Oracle bug 4275879: Introspector does not consider superinterfaces of // an interface - if (beanClass.isInterface()) { - List<PropertyDescriptor> propertyDescriptors = new ArrayList<PropertyDescriptor>(); + if (beanType.isInterface()) { + List<PropertyDescriptor> propertyDescriptors = new ArrayList<>(); - for (Class<?> cls : beanClass.getInterfaces()) { - propertyDescriptors.addAll(getBeanPropertyDescriptor(cls)); + for (Class<?> cls : beanType.getInterfaces()) { + propertyDescriptors.addAll(getBeanPropertyDescriptors(cls)); } - BeanInfo info = Introspector.getBeanInfo(beanClass); + BeanInfo info = Introspector.getBeanInfo(beanType); propertyDescriptors.addAll(getPropertyDescriptors(info)); return propertyDescriptors; } else { - BeanInfo info = Introspector.getBeanInfo(beanClass); + BeanInfo info = Introspector.getBeanInfo(beanType); return getPropertyDescriptors(info); } } /** - * Returns {@code propertyId} class for property declared in {@code clazz}. - * Property could be of form "property.subProperty[.subProperty2]" i.e. - * refer to some nested property. - * - * @param clazz - * class where property is declared - * @param propertyId - * property of form "property" or - * "property.subProperty[.subProperty2]" - * @return class of the property + * Returns the type of the property with the given name and declaring class. + * The property name may refer to a nested property, eg. + * "property.subProperty" or "property.subProperty1.subProperty2". The + * property must have a public read method (or a chain of read methods in + * case of a nested property). + * + * @param beanType + * the type declaring the property + * @param propertyName + * the name of the property + * @return the property type * @throws IntrospectionException + * if the introspection fails */ - public static Class<?> getPropertyType(Class<?> clazz, String propertyId) + public static Class<?> getPropertyType(Class<?> beanType, + String propertyName) throws IntrospectionException { - if (propertyId.contains(".")) { - String[] parts = propertyId.split("\\.", 2); - // Get the type of the field in the "cls" class - Class<?> propertyBean = getPropertyType(clazz, parts[0]); + PropertyDescriptor descriptor = getPropertyDescriptor(beanType, + propertyName); + if (descriptor != null) { + return descriptor.getPropertyType(); + } else { + return null; + } + } + + /** + * Returns the property descriptor for the property of the given name and + * declaring class. The property name may refer to a nested property, eg. + * "property.subProperty" or "property.subProperty1.subProperty2". The + * property must have a public read method (or a chain of read methods in + * case of a nested property). + * + * @param beanType + * the type declaring the property + * @param propertyName + * the name of the property + * @return the corresponding descriptor + * @throws IntrospectionException + * if the introspection fails + */ + public static PropertyDescriptor getPropertyDescriptor(Class<?> beanType, + String propertyName) throws IntrospectionException { + if (propertyName.contains(".")) { + String[] parts = propertyName.split("\\.", 2); + // Get the type of the field in the bean class + Class<?> propertyBean = getPropertyType(beanType, parts[0]); // Find the rest from the sub type - return getPropertyType(propertyBean, parts[1]); + return getPropertyDescriptor(propertyBean, parts[1]); } else { - List<PropertyDescriptor> descriptors = getBeanPropertyDescriptor( - clazz); + List<PropertyDescriptor> descriptors = getBeanPropertyDescriptors( + beanType); for (PropertyDescriptor descriptor : descriptors) { final Method getMethod = descriptor.getReadMethod(); - if (descriptor.getName().equals(propertyId) && getMethod != null + if (descriptor.getName().equals(propertyName) + && getMethod != null && getMethod.getDeclaringClass() != Object.class) { - return descriptor.getPropertyType(); + return descriptor; } } return null; @@ -118,8 +150,7 @@ public final class BeanUtil implements Serializable { private static List<PropertyDescriptor> getPropertyDescriptors( BeanInfo beanInfo) { PropertyDescriptor[] descriptors = beanInfo.getPropertyDescriptors(); - List<PropertyDescriptor> result = new ArrayList<PropertyDescriptor>( - descriptors.length); + List<PropertyDescriptor> result = new ArrayList<>(descriptors.length); for (PropertyDescriptor descriptor : descriptors) { try { Method readMethod = getMethodFromBridge( diff --git a/server/src/main/java/com/vaadin/data/validator/BeanValidator.java b/server/src/main/java/com/vaadin/data/validator/BeanValidator.java index dd563e9f0d..3813797a63 100644 --- a/server/src/main/java/com/vaadin/data/validator/BeanValidator.java +++ b/server/src/main/java/com/vaadin/data/validator/BeanValidator.java @@ -20,6 +20,7 @@ import java.util.Locale; import java.util.Objects; import java.util.Set; import java.util.function.BinaryOperator; +import java.util.logging.Logger; import javax.validation.ConstraintViolation; import javax.validation.MessageInterpolator.Context; @@ -36,10 +37,12 @@ import com.vaadin.data.Validator; * against the constraints, if any, specified by annotations on the * corresponding bean property. * <p> - * Note that a JSR-303 implementation (e.g. Hibernate Validator or Apache Bean - * Validation - formerly agimatec validation) must be present on the project - * classpath when using bean validation. - * + * Note that a JSR-303 implementation (for instance + * <a href="http://hibernate.org/validator/">Hibernate Validator</a> or + * <a href="http://bval.apache.org/">Apache BVal</a>) must be present on the + * project classpath when using bean validation. Specification versions 1.0 and + * 1.1 are supported. + * * @author Petri Hakala * @author Vaadin Ltd. * @@ -47,7 +50,7 @@ import com.vaadin.data.Validator; */ public class BeanValidator implements Validator<Object> { - private static final long serialVersionUID = 1L; + private static volatile Boolean beanValidationAvailable; private static ValidatorFactory factory; private String propertyName; @@ -55,6 +58,32 @@ public class BeanValidator implements Validator<Object> { private Locale locale; /** + * Returns whether an implementation of JSR-303 version 1.0 or 1.1 is + * present on the classpath. If this method returns false, trying to create + * a {@code BeanValidator} instance will throw an + * {@code IllegalStateException}. If an implementation is not found, logs a + * level {@code FINE} message the first time it is run. + * + * @return {@code true} if bean validation is available, {@code false} + * otherwise. + */ + public static boolean checkBeanValidationAvailable() { + if (beanValidationAvailable == null) { + try { + Class.forName(Validation.class.getName()); + beanValidationAvailable = true; + } catch (ClassNotFoundException e) { + Logger.getLogger(BeanValidator.class.getName()).fine( + "A JSR-303 bean validation implementation not found on the classpath. " + + BeanValidator.class.getSimpleName() + + " cannot be used."); + beanValidationAvailable = false; + } + } + return beanValidationAvailable; + } + + /** * Creates a new JSR-303 {@code BeanValidator} that validates values of the * specified property. Localizes validation messages using the * {@linkplain Locale#getDefault() default locale}. @@ -63,6 +92,8 @@ public class BeanValidator implements Validator<Object> { * the bean type declaring the property, not null * @param propertyName * the property to validate, not null + * @throws IllegalStateException + * if {@link #checkBeanValidationAvailable()} returns false */ public BeanValidator(Class<?> beanType, String propertyName) { this(beanType, propertyName, Locale.getDefault()); @@ -78,11 +109,19 @@ public class BeanValidator implements Validator<Object> { * the property to validate, not null * @param locale * the locale to use, not null + * @throws IllegalStateException + * if {@link #checkBeanValidationAvailable()} returns false */ public BeanValidator(Class<?> beanType, String propertyName, Locale locale) { + if (!checkBeanValidationAvailable()) { + throw new IllegalStateException("Cannot create a " + + BeanValidator.class.getSimpleName() + + ": a JSR-303 Bean Validation implementation not found on theclasspath"); + } Objects.requireNonNull(beanType, "bean class cannot be null"); Objects.requireNonNull(propertyName, "property name cannot be null"); + this.beanType = beanType; this.propertyName = propertyName; setLocale(locale); @@ -132,6 +171,7 @@ public class BeanValidator implements Validator<Object> { */ protected static ValidatorFactory getJavaxBeanValidatorFactory() { if (factory == null) { + checkBeanValidationAvailable(); factory = Validation.buildDefaultValidatorFactory(); } return factory; diff --git a/server/src/test/java/com/vaadin/data/BeanBinderTest.java b/server/src/test/java/com/vaadin/data/BeanBinderTest.java new file mode 100644 index 0000000000..a3e4d4b992 --- /dev/null +++ b/server/src/test/java/com/vaadin/data/BeanBinderTest.java @@ -0,0 +1,171 @@ +package com.vaadin.data; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertSame; + +import java.util.List; + +import org.junit.Before; +import org.junit.Test; + +import com.vaadin.tests.data.bean.BeanToValidate; +import com.vaadin.ui.TextField; + +public class BeanBinderTest { + + BeanBinder<BeanToValidate> binder; + + TextField nameField; + TextField ageField; + + BeanToValidate p = new BeanToValidate(); + + @Before + public void setUp() { + binder = new BeanBinder<>(BeanToValidate.class); + p.setFirstname("Johannes"); + p.setAge(32); + nameField = new TextField(); + ageField = new TextField(); + } + + @Test + public void fieldBound_bindBean_fieldValueUpdated() { + binder.bind(nameField, "firstname"); + binder.bind(p); + + assertEquals("Johannes", nameField.getValue()); + } + + @Test + public void beanBound_bindField_fieldValueUpdated() { + binder.bind(p); + binder.bind(nameField, "firstname"); + + assertEquals("Johannes", nameField.getValue()); + } + + @Test(expected = IllegalArgumentException.class) + public void bindInvalidPropertyName_throws() { + binder.bind(nameField, "firstnaem"); + } + + @Test(expected = NullPointerException.class) + public void bindNullPropertyName_throws() { + binder.bind(nameField, null); + } + + @Test(expected = IllegalArgumentException.class) + public void bindNonReadableProperty_throws() { + binder.bind(nameField, "writeOnlyProperty"); + } + + @Test + public void beanBound_setValidFieldValue_propertyValueChanged() { + binder.bind(p); + binder.bind(nameField, "firstname"); + + nameField.setValue("Henri"); + + assertEquals("Henri", p.getFirstname()); + } + + @Test + public void readOnlyPropertyBound_setFieldValue_ignored() { + binder.bind(nameField, "readOnlyProperty"); + binder.bind(p); + + String propertyValue = p.getReadOnlyProperty(); + nameField.setValue("Foo"); + + assertEquals(propertyValue, p.getReadOnlyProperty()); + } + + @Test + public void beanBound_setInvalidFieldValue_validationError() { + binder.bind(p); + binder.bind(nameField, "firstname"); + + nameField.setValue("H"); // too short + + assertEquals("Johannes", p.getFirstname()); + assertInvalid(nameField, "size must be between 3 and 16"); + } + + @Test + public void beanNotBound_setInvalidFieldValue_validationError() { + binder.bind(nameField, "firstname"); + + nameField.setValue("H"); // too short + + assertInvalid(nameField, "size must be between 3 and 16"); + } + + @Test + public void explicitValidatorAdded_setInvalidFieldValue_explicitValidatorRunFirst() { + binder.forField(nameField).withValidator(name -> name.startsWith("J"), + "name must start with J").bind("firstname"); + + nameField.setValue("A"); + + assertInvalid(nameField, "name must start with J"); + } + + @Test + public void explicitValidatorAdded_setInvalidFieldValue_beanValidatorRun() { + binder.forField(nameField).withValidator(name -> name.startsWith("J"), + "name must start with J").bind("firstname"); + + nameField.setValue("J"); + + assertInvalid(nameField, "size must be between 3 and 16"); + } + + @Test(expected = ClassCastException.class) + public void fieldWithIncompatibleTypeBound_bindBean_throws() { + binder.bind(ageField, "age"); + binder.bind(p); + } + + @Test(expected = ClassCastException.class) + public void fieldWithIncompatibleTypeBound_loadBean_throws() { + binder.bind(ageField, "age"); + binder.load(p); + } + + @Test(expected = ClassCastException.class) + public void fieldWithIncompatibleTypeBound_saveBean_throws() + throws Throwable { + try { + binder.bind(ageField, "age"); + binder.save(p); + } catch (RuntimeException e) { + throw e.getCause(); + } + } + + @Test + public void fieldWithConverterBound_bindBean_fieldValueUpdated() { + binder.forField(ageField).withConverter(Integer::valueOf, + String::valueOf).bind("age"); + binder.bind(p); + + assertEquals("32", ageField.getValue()); + } + + @Test(expected = ClassCastException.class) + public void fieldWithInvalidConverterBound_bindBean_fieldValueUpdated() { + binder.forField(ageField).withConverter(Float::valueOf, + String::valueOf).bind("age"); + binder.bind(p); + + assertEquals("32", ageField.getValue()); + } + + private void assertInvalid(HasValue<?> field, String message) { + List<ValidationError<?>> errors = binder.validate(); + assertEquals(1, errors.size()); + assertSame(field, errors.get(0).getField()); + assertEquals(message, errors.get(0).getMessage()); + } +} diff --git a/server/src/test/java/com/vaadin/data/BinderTest.java b/server/src/test/java/com/vaadin/data/BinderTest.java index fddae9ac99..f2c166f6ce 100644 --- a/server/src/test/java/com/vaadin/data/BinderTest.java +++ b/server/src/test/java/com/vaadin/data/BinderTest.java @@ -377,7 +377,7 @@ public class BinderTest { TextField field = new TextField(); StatusBean bean = new StatusBean(); bean.setStatus("1"); - Binder<StatusBean> binder = new Binder<StatusBean>(); + Binder<StatusBean> binder = new Binder<>(); Binding<StatusBean, String, String> binding = binder.forField(field) .withConverter(presentation -> { diff --git a/server/src/test/java/com/vaadin/tests/data/bean/BeanToValidate.java b/server/src/test/java/com/vaadin/tests/data/bean/BeanToValidate.java index fa8c438ee9..9f0b244360 100644 --- a/server/src/test/java/com/vaadin/tests/data/bean/BeanToValidate.java +++ b/server/src/test/java/com/vaadin/tests/data/bean/BeanToValidate.java @@ -42,6 +42,10 @@ public class BeanToValidate { @Valid private Address address; + private String readOnlyProperty = "READONLY DATA"; + + private String writeOnlyProperty; + public String getFirstname() { return firstname; } @@ -105,4 +109,12 @@ public class BeanToValidate { public void setAddress(Address address) { this.address = address; } + + public String getReadOnlyProperty() { + return readOnlyProperty; + } + + public void setWriteOnlyProperty(String writeOnlyProperty) { + this.writeOnlyProperty = writeOnlyProperty; + } } |