From 953e7212d84619332cba22888aa653462f9c1706 Mon Sep 17 00:00:00 2001 From: Leif Åstrand Date: Wed, 1 Feb 2017 15:30:57 +0200 Subject: Make Grid add columns based on bean properties (#8392) * Make Grid add columns based on bean properties The property set concept used for Binder is slightly generalized and used by Grid as well to support similar functionality. Fixes vaadin/framework8-issues#250 --- .../com/vaadin/data/BeanBinderPropertySet.java | 253 -------------------- .../main/java/com/vaadin/data/BeanPropertySet.java | 258 +++++++++++++++++++++ .../java/com/vaadin/data/BeanValidationBinder.java | 2 +- server/src/main/java/com/vaadin/data/Binder.java | 77 +++--- .../com/vaadin/data/BinderPropertyDefinition.java | 70 ------ .../java/com/vaadin/data/BinderPropertySet.java | 50 ---- .../java/com/vaadin/data/PropertyDefinition.java | 77 ++++++ .../src/main/java/com/vaadin/data/PropertySet.java | 51 ++++ server/src/main/java/com/vaadin/ui/Grid.java | 165 ++++++++++++- .../com/vaadin/ui/components/grid/EditorImpl.java | 8 +- 10 files changed, 600 insertions(+), 411 deletions(-) delete mode 100644 server/src/main/java/com/vaadin/data/BeanBinderPropertySet.java create mode 100644 server/src/main/java/com/vaadin/data/BeanPropertySet.java delete mode 100644 server/src/main/java/com/vaadin/data/BinderPropertyDefinition.java delete mode 100644 server/src/main/java/com/vaadin/data/BinderPropertySet.java create mode 100644 server/src/main/java/com/vaadin/data/PropertyDefinition.java create mode 100644 server/src/main/java/com/vaadin/data/PropertySet.java (limited to 'server/src/main/java/com') diff --git a/server/src/main/java/com/vaadin/data/BeanBinderPropertySet.java b/server/src/main/java/com/vaadin/data/BeanBinderPropertySet.java deleted file mode 100644 index d5fb2b4f5d..0000000000 --- a/server/src/main/java/com/vaadin/data/BeanBinderPropertySet.java +++ /dev/null @@ -1,253 +0,0 @@ -/* - * 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.io.IOException; -import java.io.Serializable; -import java.lang.reflect.InvocationTargetException; -import java.lang.reflect.Method; -import java.util.Map; -import java.util.Objects; -import java.util.Optional; -import java.util.concurrent.ConcurrentHashMap; -import java.util.concurrent.ConcurrentMap; -import java.util.function.Function; -import java.util.stream.Collectors; -import java.util.stream.Stream; - -import com.vaadin.data.util.BeanUtil; -import com.vaadin.server.Setter; -import com.vaadin.util.ReflectTools; - -/** - * A {@link BinderPropertySet} that uses reflection to find bean properties. - * - * @author Vaadin Ltd - * - * @since - * - * @param - * the type of the bean - */ -public class BeanBinderPropertySet implements BinderPropertySet { - - /** - * Serialized form of a property set. When deserialized, the property set - * for the corresponding bean type is requested, which either returns the - * existing cached instance or creates a new one. - * - * @see #readResolve() - * @see BeanBinderPropertyDefinition#writeReplace() - */ - private static class SerializedPropertySet implements Serializable { - private final Class beanType; - - private SerializedPropertySet(Class beanType) { - this.beanType = beanType; - } - - private Object readResolve() { - /* - * When this instance is deserialized, it will be replaced with a - * property set for the corresponding bean type and property name. - */ - return get(beanType); - } - } - - /** - * Serialized form of a property definition. When deserialized, the property - * set for the corresponding bean type is requested, which either returns - * the existing cached instance or creates a new one. The right property - * definition is then fetched from the property set. - * - * @see #readResolve() - * @see BeanBinderPropertySet#writeReplace() - */ - private static class SerializedPropertyDefinition implements Serializable { - private final Class beanType; - private final String propertyName; - - private SerializedPropertyDefinition(Class beanType, - String propertyName) { - this.beanType = beanType; - this.propertyName = propertyName; - } - - private Object readResolve() throws IOException { - /* - * When this instance is deserialized, it will be replaced with a - * property definition for the corresponding bean type and property - * name. - */ - return get(beanType).getProperty(propertyName) - .orElseThrow(() -> new IOException( - beanType + " no longer has a property named " - + propertyName)); - } - } - - private static class BeanBinderPropertyDefinition - implements BinderPropertyDefinition { - - private final PropertyDescriptor descriptor; - private final BeanBinderPropertySet propertySet; - - public BeanBinderPropertyDefinition( - BeanBinderPropertySet propertySet, - PropertyDescriptor descriptor) { - this.propertySet = propertySet; - this.descriptor = descriptor; - - if (descriptor.getReadMethod() == null) { - throw new IllegalArgumentException( - "Bean property has no accessible getter: " - + propertySet.beanType + "." - + descriptor.getName()); - } - - } - - @Override - public ValueProvider getGetter() { - return bean -> { - Method readMethod = descriptor.getReadMethod(); - Object value = invokeWrapExceptions(readMethod, bean); - return getType().cast(value); - }; - } - - @Override - public Optional> getSetter() { - Method setter = descriptor.getWriteMethod(); - if (setter == null) { - return Optional.empty(); - } - - return Optional.of( - (bean, value) -> invokeWrapExceptions(setter, bean, value)); - } - - @SuppressWarnings("unchecked") - @Override - public Class getType() { - return (Class) ReflectTools - .convertPrimitiveType(descriptor.getPropertyType()); - } - - @Override - public String getName() { - return descriptor.getName(); - } - - @Override - public BeanBinderPropertySet getPropertySet() { - return propertySet; - } - - private Object writeReplace() { - /* - * Instead of serializing this actual property definition, only - * serialize a DTO that when deserialized will get the corresponding - * property definition from the cache. - */ - return new SerializedPropertyDefinition(getPropertySet().beanType, - getName()); - } - } - - private static final ConcurrentMap, BeanBinderPropertySet> instances = new ConcurrentHashMap<>(); - - private final Class beanType; - - private final Map> definitions; - - private BeanBinderPropertySet(Class beanType) { - this.beanType = beanType; - - try { - definitions = BeanUtil.getBeanPropertyDescriptors(beanType).stream() - .filter(BeanBinderPropertySet::hasNonObjectReadMethod) - .map(descriptor -> new BeanBinderPropertyDefinition<>(this, - descriptor)) - .collect(Collectors.toMap(BinderPropertyDefinition::getName, - Function.identity())); - } catch (IntrospectionException e) { - throw new IllegalArgumentException( - "Cannot find property descriptors for " - + beanType.getName(), - e); - } - } - - /** - * Gets a {@link BeanBinderPropertySet} for the given bean type. - * - * @param beanType - * the bean type to get a property set for, not null - * @return the bean binder property set, not null - */ - @SuppressWarnings("unchecked") - public static BinderPropertySet get(Class beanType) { - Objects.requireNonNull(beanType, "Bean type cannot be null"); - - // Cache the reflection results - return (BinderPropertySet) instances.computeIfAbsent(beanType, - BeanBinderPropertySet::new); - } - - @Override - public Stream> getProperties() { - return definitions.values().stream(); - } - - @Override - public Optional> getProperty(String name) { - return Optional.ofNullable(definitions.get(name)); - } - - private static boolean hasNonObjectReadMethod( - PropertyDescriptor descriptor) { - Method readMethod = descriptor.getReadMethod(); - return readMethod != null - && readMethod.getDeclaringClass() != Object.class; - } - - private static Object invokeWrapExceptions(Method method, Object target, - Object... parameters) { - try { - return method.invoke(target, parameters); - } catch (IllegalAccessException | InvocationTargetException e) { - throw new RuntimeException(e); - } - } - - @Override - public String toString() { - return "Property set for bean " + beanType.getName(); - } - - private Object writeReplace() { - /* - * Instead of serializing this actual property set, only serialize a DTO - * that when deserialized will get the corresponding property set from - * the cache. - */ - return new SerializedPropertySet(beanType); - } -} diff --git a/server/src/main/java/com/vaadin/data/BeanPropertySet.java b/server/src/main/java/com/vaadin/data/BeanPropertySet.java new file mode 100644 index 0000000000..d6ab364aff --- /dev/null +++ b/server/src/main/java/com/vaadin/data/BeanPropertySet.java @@ -0,0 +1,258 @@ +/* + * 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.io.IOException; +import java.io.Serializable; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.util.Map; +import java.util.Objects; +import java.util.Optional; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ConcurrentMap; +import java.util.function.Function; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +import com.vaadin.data.util.BeanUtil; +import com.vaadin.server.Setter; +import com.vaadin.shared.util.SharedUtil; +import com.vaadin.util.ReflectTools; + +/** + * A {@link PropertySet} that uses reflection to find bean properties. + * + * @author Vaadin Ltd + * + * @since + * + * @param + * the type of the bean + */ +public class BeanPropertySet implements PropertySet { + + /** + * Serialized form of a property set. When deserialized, the property set + * for the corresponding bean type is requested, which either returns the + * existing cached instance or creates a new one. + * + * @see #readResolve() + * @see BeanPropertyDefinition#writeReplace() + */ + private static class SerializedPropertySet implements Serializable { + private final Class beanType; + + private SerializedPropertySet(Class beanType) { + this.beanType = beanType; + } + + private Object readResolve() { + /* + * When this instance is deserialized, it will be replaced with a + * property set for the corresponding bean type and property name. + */ + return get(beanType); + } + } + + /** + * Serialized form of a property definition. When deserialized, the property + * set for the corresponding bean type is requested, which either returns + * the existing cached instance or creates a new one. The right property + * definition is then fetched from the property set. + * + * @see #readResolve() + * @see BeanPropertySet#writeReplace() + */ + private static class SerializedPropertyDefinition implements Serializable { + private final Class beanType; + private final String propertyName; + + private SerializedPropertyDefinition(Class beanType, + String propertyName) { + this.beanType = beanType; + this.propertyName = propertyName; + } + + private Object readResolve() throws IOException { + /* + * When this instance is deserialized, it will be replaced with a + * property definition for the corresponding bean type and property + * name. + */ + return get(beanType).getProperty(propertyName) + .orElseThrow(() -> new IOException( + beanType + " no longer has a property named " + + propertyName)); + } + } + + private static class BeanPropertyDefinition + implements PropertyDefinition { + + private final PropertyDescriptor descriptor; + private final BeanPropertySet propertySet; + + public BeanPropertyDefinition(BeanPropertySet propertySet, + PropertyDescriptor descriptor) { + this.propertySet = propertySet; + this.descriptor = descriptor; + + if (descriptor.getReadMethod() == null) { + throw new IllegalArgumentException( + "Bean property has no accessible getter: " + + propertySet.beanType + "." + + descriptor.getName()); + } + + } + + @Override + public ValueProvider getGetter() { + return bean -> { + Method readMethod = descriptor.getReadMethod(); + Object value = invokeWrapExceptions(readMethod, bean); + return getType().cast(value); + }; + } + + @Override + public Optional> getSetter() { + Method setter = descriptor.getWriteMethod(); + if (setter == null) { + return Optional.empty(); + } + + return Optional.of( + (bean, value) -> invokeWrapExceptions(setter, bean, value)); + } + + @SuppressWarnings("unchecked") + @Override + public Class getType() { + return (Class) ReflectTools + .convertPrimitiveType(descriptor.getPropertyType()); + } + + @Override + public String getName() { + return descriptor.getName(); + } + + @Override + public String getCaption() { + return SharedUtil.propertyIdToHumanFriendly(getName()); + } + + @Override + public BeanPropertySet getPropertySet() { + return propertySet; + } + + private Object writeReplace() { + /* + * Instead of serializing this actual property definition, only + * serialize a DTO that when deserialized will get the corresponding + * property definition from the cache. + */ + return new SerializedPropertyDefinition(getPropertySet().beanType, + getName()); + } + } + + private static final ConcurrentMap, BeanPropertySet> instances = new ConcurrentHashMap<>(); + + private final Class beanType; + + private final Map> definitions; + + private BeanPropertySet(Class beanType) { + this.beanType = beanType; + + try { + definitions = BeanUtil.getBeanPropertyDescriptors(beanType).stream() + .filter(BeanPropertySet::hasNonObjectReadMethod) + .map(descriptor -> new BeanPropertyDefinition<>(this, + descriptor)) + .collect(Collectors.toMap(PropertyDefinition::getName, + Function.identity())); + } catch (IntrospectionException e) { + throw new IllegalArgumentException( + "Cannot find property descriptors for " + + beanType.getName(), + e); + } + } + + /** + * Gets a {@link BeanPropertySet} for the given bean type. + * + * @param beanType + * the bean type to get a property set for, not null + * @return the bean property set, not null + */ + @SuppressWarnings("unchecked") + public static PropertySet get(Class beanType) { + Objects.requireNonNull(beanType, "Bean type cannot be null"); + + // Cache the reflection results + return (PropertySet) instances.computeIfAbsent(beanType, + BeanPropertySet::new); + } + + @Override + public Stream> getProperties() { + return definitions.values().stream(); + } + + @Override + public Optional> getProperty(String name) { + return Optional.ofNullable(definitions.get(name)); + } + + private static boolean hasNonObjectReadMethod( + PropertyDescriptor descriptor) { + Method readMethod = descriptor.getReadMethod(); + return readMethod != null + && readMethod.getDeclaringClass() != Object.class; + } + + private static Object invokeWrapExceptions(Method method, Object target, + Object... parameters) { + try { + return method.invoke(target, parameters); + } catch (IllegalAccessException | InvocationTargetException e) { + throw new RuntimeException(e); + } + } + + @Override + public String toString() { + return "Property set for bean " + beanType.getName(); + } + + private Object writeReplace() { + /* + * Instead of serializing this actual property set, only serialize a DTO + * that when deserialized will get the corresponding property set from + * the cache. + */ + return new SerializedPropertySet(beanType); + } +} diff --git a/server/src/main/java/com/vaadin/data/BeanValidationBinder.java b/server/src/main/java/com/vaadin/data/BeanValidationBinder.java index 5e3b220ffd..34af4156a4 100644 --- a/server/src/main/java/com/vaadin/data/BeanValidationBinder.java +++ b/server/src/main/java/com/vaadin/data/BeanValidationBinder.java @@ -56,7 +56,7 @@ public class BeanValidationBinder extends Binder { @Override protected BindingBuilder configureBinding( BindingBuilder binding, - BinderPropertyDefinition definition) { + PropertyDefinition definition) { return binding.withValidator( new BeanValidator(beanType, definition.getName())); } diff --git a/server/src/main/java/com/vaadin/data/Binder.java b/server/src/main/java/com/vaadin/data/Binder.java index f291c791b7..d36c9996fd 100644 --- a/server/src/main/java/com/vaadin/data/Binder.java +++ b/server/src/main/java/com/vaadin/data/Binder.java @@ -23,7 +23,6 @@ import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; import java.util.HashMap; -import java.util.HashSet; import java.util.IdentityHashMap; import java.util.LinkedHashSet; import java.util.List; @@ -195,7 +194,7 @@ public class Binder implements Serializable { /** * Completes this binding by connecting the field to the property with * the given name. The getter and setter of the property are looked up - * using a {@link BinderPropertySet}. + * using a {@link PropertySet}. *

* For a Binder created using the * {@link Binder#Binder(Class)} constructor, introspection will be used @@ -217,7 +216,7 @@ public class Binder implements Serializable { * if the property has no accessible getter * @throws IllegalStateException * if the binder is not configured with an appropriate - * {@link BinderPropertySet} + * {@link PropertySet} * * @see Binder.BindingBuilder#bind(ValueProvider, Setter) */ @@ -608,7 +607,7 @@ public class Binder implements Serializable { "Property name cannot be null"); checkUnbound(); - BinderPropertyDefinition definition = getBinder().propertySet + PropertyDefinition definition = getBinder().propertySet .getProperty(propertyName) .orElseThrow(() -> new IllegalArgumentException( "Could not resolve property name " + propertyName @@ -627,9 +626,11 @@ public class Binder implements Serializable { definition); try { - return ((BindingBuilder) finalBinding).bind(getter, setter); + Binding binding = ((BindingBuilder) finalBinding).bind(getter, + setter); + getBinder().boundProperties.put(propertyName, binding); + return binding; } finally { - getBinder().boundProperties.add(propertyName); getBinder().incompleteMemberFieldBindings.remove(getField()); } } @@ -1044,12 +1045,12 @@ public class Binder implements Serializable { } } - private final BinderPropertySet propertySet; + private final PropertySet propertySet; /** * Property names that have been used for creating a binding. */ - private final Set boundProperties = new HashSet<>(); + private final Map> boundProperties = new HashMap<>(); private final Map, BindingBuilder> incompleteMemberFieldBindings = new IdentityHashMap<>(); @@ -1072,16 +1073,15 @@ public class Binder implements Serializable { private boolean hasChanges = false; /** - * Creates a binder using a custom {@link BinderPropertySet} implementation - * for finding and resolving property names for + * Creates a binder using a custom {@link PropertySet} implementation for + * finding and resolving property names for * {@link #bindInstanceFields(Object)}, {@link #bind(HasValue, String)} and * {@link BindingBuilder#bind(String)}. * * @param propertySet - * the binder property set implementation to use, not - * null. + * the property set implementation to use, not null. */ - protected Binder(BinderPropertySet propertySet) { + protected Binder(PropertySet propertySet) { Objects.requireNonNull(propertySet, "propertySet cannot be null"); this.propertySet = propertySet; } @@ -1094,7 +1094,7 @@ public class Binder implements Serializable { * the bean type to use, not null */ public Binder(Class beanType) { - this(BeanBinderPropertySet.get(beanType)); + this(BeanPropertySet.get(beanType)); } /** @@ -1106,15 +1106,15 @@ public class Binder implements Serializable { * {@link #bind(HasValue, String)} or {@link BindingBuilder#bind(String)}. */ public Binder() { - this(new BinderPropertySet() { + this(new PropertySet() { @Override - public Stream> getProperties() { + public Stream> getProperties() { throw new IllegalStateException( "A Binder created with the default constructor doesn't support listing properties."); } @Override - public Optional> getProperty( + public Optional> getProperty( String name) { throw new IllegalStateException( "A Binder created with the default constructor doesn't support finding properties by name."); @@ -1123,8 +1123,8 @@ public class Binder implements Serializable { } /** - * Creates a binder using a custom {@link BinderPropertySet} implementation - * for finding and resolving property names for + * Creates a binder using a custom {@link PropertySet} implementation for + * finding and resolving property names for * {@link #bindInstanceFields(Object)}, {@link #bind(HasValue, String)} and * {@link BindingBuilder#bind(String)}. *

@@ -1137,13 +1137,12 @@ public class Binder implements Serializable { * @see Binder#Binder(Class) * * @param propertySet - * the binder property set implementation to use, not - * null. + * the property set implementation to use, not null. * @return a new binder using the provided property set, not * null */ public static Binder withPropertySet( - BinderPropertySet propertySet) { + PropertySet propertySet) { return new Binder<>(propertySet); } @@ -1272,7 +1271,7 @@ public class Binder implements Serializable { /** * Binds the given field to the property with the given name. The getter and - * setter of the property are looked up using a {@link BinderPropertySet}. + * setter of the property are looked up using a {@link PropertySet}. *

* For a Binder created using the {@link Binder#Binder(Class)} * constructor, introspection will be used to find a Java Bean property. If @@ -1297,7 +1296,7 @@ public class Binder implements Serializable { * if the property has no accessible getter * @throws IllegalStateException * if the binder is not configured with an appropriate - * {@link BinderPropertySet} + * {@link PropertySet} * * @see #bind(HasValue, ValueProvider, Setter) */ @@ -1964,7 +1963,7 @@ public class Binder implements Serializable { /** * Configures the {@code binding} with the property definition * {@code definition} before it's being bound. - * + * * @param binding * a binding to configure * @param definition @@ -1973,7 +1972,7 @@ public class Binder implements Serializable { */ protected BindingBuilder configureBinding( BindingBuilder binding, - BinderPropertyDefinition definition) { + PropertyDefinition definition) { return binding; } @@ -2217,7 +2216,7 @@ public class Binder implements Serializable { private void handleProperty(Field field, Object objectWithMemberFields, BiConsumer> propertyHandler) { - Optional> descriptor = getPropertyDescriptor( + Optional> descriptor = getPropertyDescriptor( field); if (!descriptor.isPresent()) { @@ -2225,7 +2224,7 @@ public class Binder implements Serializable { } String propertyName = descriptor.get().getName(); - if (boundProperties.contains(propertyName)) { + if (boundProperties.containsKey(propertyName)) { return; } @@ -2237,10 +2236,25 @@ public class Binder implements Serializable { } propertyHandler.accept(propertyName, descriptor.get().getType()); - boundProperties.add(propertyName); + assert boundProperties.containsKey(propertyName); + } + + /** + * Gets the binding for a property name. Bindings are available by property + * name if bound using {@link #bind(HasValue, String)}, + * {@link BindingBuilder#bind(String)} or indirectly using + * {@link #bindInstanceFields(Object)}. + * + * @param propertyName + * the property name of the binding to get + * @return the binding corresponding to the property name, or an empty + * optional if there is no binding with that property name + */ + public Optional> getBinding(String propertyName) { + return Optional.ofNullable(boundProperties.get(propertyName)); } - private Optional> getPropertyDescriptor( + private Optional> getPropertyDescriptor( Field field) { PropertyId propertyIdAnnotation = field.getAnnotation(PropertyId.class); @@ -2254,8 +2268,7 @@ public class Binder implements Serializable { String minifiedFieldName = minifyFieldName(propertyId); - return propertySet.getProperties() - .map(BinderPropertyDefinition::getName) + return propertySet.getProperties().map(PropertyDefinition::getName) .filter(name -> minifyFieldName(name).equals(minifiedFieldName)) .findFirst().flatMap(propertySet::getProperty); } diff --git a/server/src/main/java/com/vaadin/data/BinderPropertyDefinition.java b/server/src/main/java/com/vaadin/data/BinderPropertyDefinition.java deleted file mode 100644 index b4145a8c4f..0000000000 --- a/server/src/main/java/com/vaadin/data/BinderPropertyDefinition.java +++ /dev/null @@ -1,70 +0,0 @@ -/* - * Copyright 2000-2016 Vaadin Ltd. - * - * Licensed under the Apache License, Version 2.0 (the "License"); you may not - * use this file except in compliance with the License. You may obtain a copy of - * the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT - * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the - * License for the specific language governing permissions and limitations under - * the License. - */ -package com.vaadin.data; - -import java.io.Serializable; -import java.util.Optional; - -import com.vaadin.server.Setter; - -/** - * A property from a {@link BinderPropertySet}. - * - * @author Vaadin Ltd - * @since - * - * @param - * the type of the binder property set - * @param - * the property type - */ -public interface BinderPropertyDefinition extends Serializable { - /** - * Gets the value provider that is used for finding the value of this - * property for a bean. - * - * @return the getter, not null - */ - public ValueProvider getGetter(); - - /** - * Gets an optional setter for storing a property value in a bean. - * - * @return the setter, or an empty optional if this property is read-only - */ - public Optional> getSetter(); - - /** - * Gets the type of this property. - * - * @return the property type. not null - */ - public Class getType(); - - /** - * Gets the name of this property. - * - * @return the property name, not null - */ - public String getName(); - - /** - * Gets the {@link BinderPropertySet} that this property belongs to. - * - * @return the binder property set, not null - */ - public BinderPropertySet getPropertySet(); -} diff --git a/server/src/main/java/com/vaadin/data/BinderPropertySet.java b/server/src/main/java/com/vaadin/data/BinderPropertySet.java deleted file mode 100644 index 6252a228aa..0000000000 --- a/server/src/main/java/com/vaadin/data/BinderPropertySet.java +++ /dev/null @@ -1,50 +0,0 @@ -/* - * Copyright 2000-2016 Vaadin Ltd. - * - * Licensed under the Apache License, Version 2.0 (the "License"); you may not - * use this file except in compliance with the License. You may obtain a copy of - * the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT - * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the - * License for the specific language governing permissions and limitations under - * the License. - */ -package com.vaadin.data; - -import java.io.Serializable; -import java.util.Optional; -import java.util.stream.Stream; - -/** - * Describes a set of properties that can be used with a {@link Binder}. - * - * @author Vaadin Ltd - * - * @since - * - * @param - * the type for which the properties are defined - */ -public interface BinderPropertySet extends Serializable { - /** - * Gets all known properties as a stream. - * - * @return a stream of property names, not null - */ - public Stream> getProperties(); - - /** - * Gets the definition for the named property, or an empty optional if there - * is no property with the given name. - * - * @param name - * the property name to look for, not null - * @return the property definition, or empty optional if property doesn't - * exist - */ - public Optional> getProperty(String name); -} diff --git a/server/src/main/java/com/vaadin/data/PropertyDefinition.java b/server/src/main/java/com/vaadin/data/PropertyDefinition.java new file mode 100644 index 0000000000..79bb2159b4 --- /dev/null +++ b/server/src/main/java/com/vaadin/data/PropertyDefinition.java @@ -0,0 +1,77 @@ +/* + * Copyright 2000-2016 Vaadin Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ +package com.vaadin.data; + +import java.io.Serializable; +import java.util.Optional; + +import com.vaadin.server.Setter; + +/** + * A property from a {@link PropertySet}. + * + * @author Vaadin Ltd + * @since + * + * @param + * the type of the property set + * @param + * the property type + */ +public interface PropertyDefinition extends Serializable { + /** + * Gets the value provider that is used for finding the value of this + * property for a bean. + * + * @return the getter, not null + */ + public ValueProvider getGetter(); + + /** + * Gets an optional setter for storing a property value in a bean. + * + * @return the setter, or an empty optional if this property is read-only + */ + public Optional> getSetter(); + + /** + * Gets the type of this property. + * + * @return the property type. not null + */ + public Class getType(); + + /** + * Gets the name of this property. + * + * @return the property name, not null + */ + public String getName(); + + /** + * Gets the human readable caption to show for this property. + * + * @return the caption to show, not null + */ + public String getCaption(); + + /** + * Gets the {@link PropertySet} that this property belongs to. + * + * @return the property set, not null + */ + public PropertySet getPropertySet(); +} diff --git a/server/src/main/java/com/vaadin/data/PropertySet.java b/server/src/main/java/com/vaadin/data/PropertySet.java new file mode 100644 index 0000000000..7b557dc293 --- /dev/null +++ b/server/src/main/java/com/vaadin/data/PropertySet.java @@ -0,0 +1,51 @@ +/* + * Copyright 2000-2016 Vaadin Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ +package com.vaadin.data; + +import java.io.Serializable; +import java.util.Optional; +import java.util.stream.Stream; + +/** + * Describes a set of properties that can be used for configuration based on + * property names instead of setter and getter callbacks. + * + * @author Vaadin Ltd + * + * @since + * + * @param + * the type for which the properties are defined + */ +public interface PropertySet extends Serializable { + /** + * Gets all known properties as a stream. + * + * @return a stream of property names, not null + */ + public Stream> getProperties(); + + /** + * Gets the definition for the named property, or an empty optional if there + * is no property with the given name. + * + * @param name + * the property name to look for, not null + * @return the property definition, or empty optional if property doesn't + * exist + */ + public Optional> getProperty(String name); +} diff --git a/server/src/main/java/com/vaadin/ui/Grid.java b/server/src/main/java/com/vaadin/ui/Grid.java index 4052282fdc..daf1cd1c69 100644 --- a/server/src/main/java/com/vaadin/ui/Grid.java +++ b/server/src/main/java/com/vaadin/ui/Grid.java @@ -41,10 +41,13 @@ import org.jsoup.nodes.Attributes; import org.jsoup.nodes.Element; import org.jsoup.select.Elements; +import com.vaadin.data.BeanPropertySet; import com.vaadin.data.Binder; import com.vaadin.data.Binder.Binding; import com.vaadin.data.HasDataProvider; import com.vaadin.data.HasValue; +import com.vaadin.data.PropertyDefinition; +import com.vaadin.data.PropertySet; import com.vaadin.data.ValueProvider; import com.vaadin.data.provider.DataCommunicator; import com.vaadin.data.provider.DataProvider; @@ -1609,7 +1612,9 @@ public class Grid extends AbstractListing implements HasComponents, * a setter that stores the component value in the row item * @return this column * + * @see #setEditorBinding(Binding) * @see Grid#getEditor() + * @see Binder#bind(HasValue, ValueProvider, Setter) */ public & Component> Column setEditorComponent( C editorComponent, Setter setter) { @@ -1623,6 +1628,47 @@ public class Grid extends AbstractListing implements HasComponents, return setEditorBinding(binding); } + /** + * Sets a component to use for editing values of this columns in the + * editor row. This method can only be used if the column has an + * {@link #setId(String) id} and the {@link Grid} has been created using + * {@link Grid#Grid(Class)} or some other way that allows finding + * properties based on property names. + *

+ * This is a shorthand for use in simple cases where no validator or + * converter is needed. Use {@link #setEditorBinding(Binding)} to + * support more complex cases. + *

+ * Note: The same component cannot be used for multiple + * columns. + * + * @param editorComponent + * the editor component + * @return this column + * + * @see #setEditorBinding(Binding) + * @see Grid#getEditor() + * @see Binder#bind(HasValue, String) + * @see Grid#Grid(Class) + */ + public & Component> Column setEditorComponent( + C editorComponent) { + Objects.requireNonNull(editorComponent, + "Editor component cannot be null"); + + String propertyName = getId(); + if (propertyName == null) { + throw new IllegalStateException( + "setEditorComponent without a setter can only be used if the column has an id. " + + "Use another setEditorComponent(Component, Setter) or setEditorBinding(Binding) instead."); + } + + Binding binding = getGrid().getEditor().getBinder() + .bind(editorComponent, propertyName); + + return setEditorBinding(binding); + } + /** * Gets the grid that this column belongs to. * @@ -1851,10 +1897,64 @@ public class Grid extends AbstractListing implements HasComponents, private Editor editor; + private final PropertySet propertySet; + /** - * Constructor for the {@link Grid} component. + * Creates a new grid without support for creating columns based on property + * names. Use an alternative constructor, such as {@link Grid#Grid(Class)}, + * to create a grid that automatically sets up columns based on the type of + * presented data. + * + * @see #Grid(Class) + * @see #withPropertySet(PropertySet) */ public Grid() { + this(new PropertySet() { + @Override + public Stream> getProperties() { + // No columns configured by default + return Stream.empty(); + } + + @Override + public Optional> getProperty(String name) { + throw new IllegalStateException( + "A Grid created without a bean type class literal or a custom property set" + + " doesn't support finding properties by name."); + } + }); + } + + /** + * Creates a new grid that uses reflection based on the provided bean type + * to automatically set up an initial set of columns. All columns will be + * configured using the same {@link Object#toString()} renderer that is used + * by {@link #addColumn(ValueProvider)}. + * + * @param beanType + * the bean type to use, not null + * @see #Grid() + * @see #withPropertySet(PropertySet) + */ + public Grid(Class beanType) { + this(BeanPropertySet.get(beanType)); + } + + /** + * Creates a grid using a custom {@link PropertySet} implementation for + * configuring the initial columns and resolving property names for + * {@link #addColumn(String)} and + * {@link Column#setEditorComponent(HasValue)}. + * + * @see #withPropertySet(PropertySet) + * + * @param propertySet + * the property set implementation to use, not null. + */ + protected Grid(PropertySet propertySet) { + Objects.requireNonNull(propertySet, "propertySet cannot be null"); + this.propertySet = propertySet; + registerRpc(new GridServerRpcImpl()); setDefaultHeaderRow(appendHeaderRow()); @@ -1882,6 +1982,33 @@ public class Grid extends AbstractListing implements HasComponents, } } }); + + // Automatically add columns for all available properties + propertySet.getProperties().map(PropertyDefinition::getName) + .forEach(this::addColumn); + } + + /** + * Creates a grid using a custom {@link PropertySet} implementation for + * creating a default set of columns and for resolving property names with + * {@link #addColumn(String)} and + * {@link Column#setEditorComponent(HasValue)}. + *

+ * This functionality is provided as static method instead of as a public + * constructor in order to make it possible to use a custom property set + * without creating a subclass while still leaving the public constructors + * focused on the common use cases. + * + * @see Grid#Grid() + * @see Grid#Grid(Class) + * + * @param propertySet + * the property set implementation to use, not null. + * @return a new grid using the provided property set, not null + */ + public static Grid withPropertySet( + PropertySet propertySet) { + return new Grid<>(propertySet); } /** @@ -1939,6 +2066,37 @@ public class Grid extends AbstractListing implements HasComponents, userOriginated)); } + /** + * Adds a new column with the given property name. The property name will be + * used as the {@link Column#getId() column id} and the + * {@link Column#getCaption() column caption} will be set based on the + * property definition. + *

+ * This method can only be used for a Grid created using + * {@link Grid#Grid(Class)} or {@link #withPropertySet(PropertySet)}. + * + * @param propertyName + * the property name of the new column, not null + * @return the newly added column, not null + */ + public Column addColumn(String propertyName) { + Objects.requireNonNull(propertyName, "Property name cannot be null"); + + if (getColumn(propertyName) != null) { + throw new IllegalStateException( + "There is already a column for " + propertyName); + } + + PropertyDefinition definition = propertySet + .getProperty(propertyName) + .orElseThrow(() -> new IllegalArgumentException( + "Could not resolve property name " + propertyName + + " from " + propertySet)); + + return addColumn(definition.getGetter()).setId(definition.getName()) + .setCaption(definition.getCaption()); + } + /** * Adds a new text column to this {@link Grid} with a value provider. The * column will use a {@link TextRenderer}. The value is converted to a @@ -2070,7 +2228,8 @@ public class Grid extends AbstractListing implements HasComponents, * * @param columnId * the identifier of the column to get - * @return the column corresponding to the given column identifier + * @return the column corresponding to the given column identifier, or + * null if there is no such column */ public Column getColumn(String columnId) { return columnIds.get(columnId); @@ -2983,7 +3142,7 @@ public class Grid extends AbstractListing implements HasComponents, * @return editor */ protected Editor createEditor() { - return new EditorImpl<>(); + return new EditorImpl<>(propertySet); } private void addExtensionComponent(Component c) { diff --git a/server/src/main/java/com/vaadin/ui/components/grid/EditorImpl.java b/server/src/main/java/com/vaadin/ui/components/grid/EditorImpl.java index a001a5026a..dae7c61ee7 100644 --- a/server/src/main/java/com/vaadin/ui/components/grid/EditorImpl.java +++ b/server/src/main/java/com/vaadin/ui/components/grid/EditorImpl.java @@ -27,6 +27,7 @@ import com.vaadin.data.Binder; import com.vaadin.data.Binder.Binding; import com.vaadin.data.BinderValidationStatus; import com.vaadin.data.BinderValidationStatusHandler; +import com.vaadin.data.PropertySet; import com.vaadin.shared.ui.grid.editor.EditorClientRpc; import com.vaadin.shared.ui.grid.editor.EditorServerRpc; import com.vaadin.shared.ui.grid.editor.EditorState; @@ -112,8 +113,11 @@ public class EditorImpl extends AbstractGridExtension /** * Constructor for internal implementation of the Editor. + * + * @param propertySet + * the property set to use for configuring the default binder */ - public EditorImpl() { + public EditorImpl(PropertySet propertySet) { rpc = getRpcProxy(EditorClientRpc.class); registerRpc(new EditorServerRpc() { @@ -142,7 +146,7 @@ public class EditorImpl extends AbstractGridExtension } }); - setBinder(new Binder<>()); + setBinder(Binder.withPropertySet(propertySet)); } @Override -- cgit v1.2.3