diff options
author | Leif Åstrand <legioth@gmail.com> | 2017-02-01 15:30:57 +0200 |
---|---|---|
committer | Pekka Hyvönen <pekka@vaadin.com> | 2017-02-01 15:30:57 +0200 |
commit | 953e7212d84619332cba22888aa653462f9c1706 (patch) | |
tree | 08ff65e0d812dc507dcf816c5c49743256eeff23 | |
parent | 38b475330868d2d7b0d0b2da0a14be4040ca89ae (diff) | |
download | vaadin-framework-953e7212d84619332cba22888aa653462f9c1706.tar.gz vaadin-framework-953e7212d84619332cba22888aa653462f9c1706.zip |
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
12 files changed, 404 insertions, 110 deletions
diff --git a/documentation/components/components-grid.asciidoc b/documentation/components/components-grid.asciidoc index 3ea0e578f3..52d2301a46 100644 --- a/documentation/components/components-grid.asciidoc +++ b/documentation/components/components-grid.asciidoc @@ -42,7 +42,7 @@ cell style generator. [[components.grid.data]] == Binding to Data -[classname]#Grid# is normally used by binding it to a , +[classname]#Grid# is normally used by binding it to a data provider, described in <<dummy/../../../framework/datamodel/datamodel-providers.asciidoc#datamodel.dataproviders,"Showing Many Items in a Listing">>. By default, it is bound to List of items. You can set the items with the @@ -96,7 +96,7 @@ grid.setSelectionMode(SelectionMode.MULTI); grid.addSelectionListener(event -> { Set<Person> selected = event.getAllSelectedItems(); Notification.show(selected.size() + " items selected"); -} +}); ---- Programmatically selecting the value is possible via [methodname]#select(T)#. @@ -214,7 +214,7 @@ selectionModel.addMultiSelectionListener(event -> { // Allow deleting only if there's any selected deleteSelected.setEnabled( event.getNewSelection().size() > 0); -}; +}); ---- @@ -241,7 +241,7 @@ and column. [source, java] ---- grid.addCellClickListener(event -> - Notification.show("Value: " + event.getItem()); + Notification.show("Value: " + event.getItem())); ---- The clicked grid cell is also automatically focused. @@ -255,15 +255,11 @@ well as disable cell focus, in a custom theme. See <<components.grid.css>>. [[components.grid.columns]] == Configuring Columns -Columns are normally defined in the container data source. The -[methodname]#addColumn()# method can be used to add columns to [classname]#Grid#. - -Column configuration is defined in [classname]#Grid.Column# objects, which can -be obtained from the grid with [methodname]#getColumns()#. +The [methodname]#addColumn()# method can be used to add columns to [classname]#Grid#. -The setter methods in [classname]#Column# have _fluent API_, so you can easily chain -the configuration calls for columns if you want to. +Column configuration is defined in [classname]#Grid.Column# objects, which are returned by `addColumn` and can also be obtained from the grid with [methodname]#getColumns()#. +The setter methods in [classname]#Column# have _fluent API_, so you can easily chain the configuration calls for columns if you want to. [source, java] ---- @@ -275,6 +271,22 @@ grid.addColumn(Person:getBirthDate, new DateRenderer()) In the following, we describe the basic column configuration. +[[components.grid.columns.automatic]] +=== Automatically Adding Columns + +You can configure `Grid` to automatically add columns based on the properties in a bean. +To do this, you need to pass the `Class` of the bean type to the constructor when creating a grid. +You can then further configure the columns based on the bean property name. + +[source, java] +---- +Grid<Person> grid = new Grid<>(Person.class); + +grid.getColumn("birthDate").setWidth("100px"); + +grid.setItems(people); +---- + [[components.grid.columns.order]] === Column Order @@ -424,7 +436,7 @@ grid.addColumn(person -> "Delete", new ButtonRenderer(clickEvent -> { people.remove(clickEvent.getValue()); grid.setItems(people); - }); + })); ---- [classname]#ImageRenderer#:: Renders the cell as an image. diff --git a/server/src/main/java/com/vaadin/data/BeanBinderPropertySet.java b/server/src/main/java/com/vaadin/data/BeanPropertySet.java index d5fb2b4f5d..d6ab364aff 100644 --- a/server/src/main/java/com/vaadin/data/BeanBinderPropertySet.java +++ b/server/src/main/java/com/vaadin/data/BeanPropertySet.java @@ -32,10 +32,11 @@ 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 BinderPropertySet} that uses reflection to find bean properties. + * A {@link PropertySet} that uses reflection to find bean properties. * * @author Vaadin Ltd * @@ -44,7 +45,7 @@ import com.vaadin.util.ReflectTools; * @param <T> * the type of the bean */ -public class BeanBinderPropertySet<T> implements BinderPropertySet<T> { +public class BeanPropertySet<T> implements PropertySet<T> { /** * Serialized form of a property set. When deserialized, the property set @@ -52,7 +53,7 @@ public class BeanBinderPropertySet<T> implements BinderPropertySet<T> { * existing cached instance or creates a new one. * * @see #readResolve() - * @see BeanBinderPropertyDefinition#writeReplace() + * @see BeanPropertyDefinition#writeReplace() */ private static class SerializedPropertySet implements Serializable { private final Class<?> beanType; @@ -77,7 +78,7 @@ public class BeanBinderPropertySet<T> implements BinderPropertySet<T> { * definition is then fetched from the property set. * * @see #readResolve() - * @see BeanBinderPropertySet#writeReplace() + * @see BeanPropertySet#writeReplace() */ private static class SerializedPropertyDefinition implements Serializable { private final Class<?> beanType; @@ -102,14 +103,13 @@ public class BeanBinderPropertySet<T> implements BinderPropertySet<T> { } } - private static class BeanBinderPropertyDefinition<T, V> - implements BinderPropertyDefinition<T, V> { + private static class BeanPropertyDefinition<T, V> + implements PropertyDefinition<T, V> { private final PropertyDescriptor descriptor; - private final BeanBinderPropertySet<T> propertySet; + private final BeanPropertySet<T> propertySet; - public BeanBinderPropertyDefinition( - BeanBinderPropertySet<T> propertySet, + public BeanPropertyDefinition(BeanPropertySet<T> propertySet, PropertyDescriptor descriptor) { this.propertySet = propertySet; this.descriptor = descriptor; @@ -156,7 +156,12 @@ public class BeanBinderPropertySet<T> implements BinderPropertySet<T> { } @Override - public BeanBinderPropertySet<T> getPropertySet() { + public String getCaption() { + return SharedUtil.propertyIdToHumanFriendly(getName()); + } + + @Override + public BeanPropertySet<T> getPropertySet() { return propertySet; } @@ -171,21 +176,21 @@ public class BeanBinderPropertySet<T> implements BinderPropertySet<T> { } } - private static final ConcurrentMap<Class<?>, BeanBinderPropertySet<?>> instances = new ConcurrentHashMap<>(); + private static final ConcurrentMap<Class<?>, BeanPropertySet<?>> instances = new ConcurrentHashMap<>(); private final Class<T> beanType; - private final Map<String, BinderPropertyDefinition<T, ?>> definitions; + private final Map<String, PropertyDefinition<T, ?>> definitions; - private BeanBinderPropertySet(Class<T> beanType) { + private BeanPropertySet(Class<T> beanType) { this.beanType = beanType; try { definitions = BeanUtil.getBeanPropertyDescriptors(beanType).stream() - .filter(BeanBinderPropertySet::hasNonObjectReadMethod) - .map(descriptor -> new BeanBinderPropertyDefinition<>(this, + .filter(BeanPropertySet::hasNonObjectReadMethod) + .map(descriptor -> new BeanPropertyDefinition<>(this, descriptor)) - .collect(Collectors.toMap(BinderPropertyDefinition::getName, + .collect(Collectors.toMap(PropertyDefinition::getName, Function.identity())); } catch (IntrospectionException e) { throw new IllegalArgumentException( @@ -196,28 +201,28 @@ public class BeanBinderPropertySet<T> implements BinderPropertySet<T> { } /** - * Gets a {@link BeanBinderPropertySet} for the given bean type. + * Gets a {@link BeanPropertySet} for the given bean type. * * @param beanType * the bean type to get a property set for, not <code>null</code> - * @return the bean binder property set, not <code>null</code> + * @return the bean property set, not <code>null</code> */ @SuppressWarnings("unchecked") - public static <T> BinderPropertySet<T> get(Class<? extends T> beanType) { + public static <T> PropertySet<T> get(Class<? extends T> beanType) { Objects.requireNonNull(beanType, "Bean type cannot be null"); // Cache the reflection results - return (BinderPropertySet<T>) instances.computeIfAbsent(beanType, - BeanBinderPropertySet::new); + return (PropertySet<T>) instances.computeIfAbsent(beanType, + BeanPropertySet::new); } @Override - public Stream<BinderPropertyDefinition<T, ?>> getProperties() { + public Stream<PropertyDefinition<T, ?>> getProperties() { return definitions.values().stream(); } @Override - public Optional<BinderPropertyDefinition<T, ?>> getProperty(String name) { + public Optional<PropertyDefinition<T, ?>> getProperty(String name) { return Optional.ofNullable(definitions.get(name)); } 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<BEAN> extends Binder<BEAN> { @Override protected BindingBuilder<BEAN, ?> configureBinding( BindingBuilder<BEAN, ?> binding, - BinderPropertyDefinition<BEAN, ?> definition) { + PropertyDefinition<BEAN, ?> 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<BEAN> 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}. * <p> * For a <code>Binder</code> created using the * {@link Binder#Binder(Class)} constructor, introspection will be used @@ -217,7 +216,7 @@ public class Binder<BEAN> 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<BEAN> implements Serializable { "Property name cannot be null"); checkUnbound(); - BinderPropertyDefinition<BEAN, ?> definition = getBinder().propertySet + PropertyDefinition<BEAN, ?> definition = getBinder().propertySet .getProperty(propertyName) .orElseThrow(() -> new IllegalArgumentException( "Could not resolve property name " + propertyName @@ -627,9 +626,11 @@ public class Binder<BEAN> 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<BEAN> implements Serializable { } } - private final BinderPropertySet<BEAN> propertySet; + private final PropertySet<BEAN> propertySet; /** * Property names that have been used for creating a binding. */ - private final Set<String> boundProperties = new HashSet<>(); + private final Map<String, Binding<BEAN, ?>> boundProperties = new HashMap<>(); private final Map<HasValue<?>, BindingBuilder<BEAN, ?>> incompleteMemberFieldBindings = new IdentityHashMap<>(); @@ -1072,16 +1073,15 @@ public class Binder<BEAN> 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 - * <code>null</code>. + * the property set implementation to use, not <code>null</code>. */ - protected Binder(BinderPropertySet<BEAN> propertySet) { + protected Binder(PropertySet<BEAN> propertySet) { Objects.requireNonNull(propertySet, "propertySet cannot be null"); this.propertySet = propertySet; } @@ -1094,7 +1094,7 @@ public class Binder<BEAN> implements Serializable { * the bean type to use, not <code>null</code> */ public Binder(Class<BEAN> beanType) { - this(BeanBinderPropertySet.get(beanType)); + this(BeanPropertySet.get(beanType)); } /** @@ -1106,15 +1106,15 @@ public class Binder<BEAN> implements Serializable { * {@link #bind(HasValue, String)} or {@link BindingBuilder#bind(String)}. */ public Binder() { - this(new BinderPropertySet<BEAN>() { + this(new PropertySet<BEAN>() { @Override - public Stream<BinderPropertyDefinition<BEAN, ?>> getProperties() { + public Stream<PropertyDefinition<BEAN, ?>> getProperties() { throw new IllegalStateException( "A Binder created with the default constructor doesn't support listing properties."); } @Override - public Optional<BinderPropertyDefinition<BEAN, ?>> getProperty( + public Optional<PropertyDefinition<BEAN, ?>> 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<BEAN> 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)}. * <p> @@ -1137,13 +1137,12 @@ public class Binder<BEAN> implements Serializable { * @see Binder#Binder(Class) * * @param propertySet - * the binder property set implementation to use, not - * <code>null</code>. + * the property set implementation to use, not <code>null</code>. * @return a new binder using the provided property set, not * <code>null</code> */ public static <BEAN> Binder<BEAN> withPropertySet( - BinderPropertySet<BEAN> propertySet) { + PropertySet<BEAN> propertySet) { return new Binder<>(propertySet); } @@ -1272,7 +1271,7 @@ public class Binder<BEAN> 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}. * <p> * For a <code>Binder</code> 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<BEAN> 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<BEAN> 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<BEAN> implements Serializable { */ protected BindingBuilder<BEAN, ?> configureBinding( BindingBuilder<BEAN, ?> binding, - BinderPropertyDefinition<BEAN, ?> definition) { + PropertyDefinition<BEAN, ?> definition) { return binding; } @@ -2217,7 +2216,7 @@ public class Binder<BEAN> implements Serializable { private void handleProperty(Field field, Object objectWithMemberFields, BiConsumer<String, Class<?>> propertyHandler) { - Optional<BinderPropertyDefinition<BEAN, ?>> descriptor = getPropertyDescriptor( + Optional<PropertyDefinition<BEAN, ?>> descriptor = getPropertyDescriptor( field); if (!descriptor.isPresent()) { @@ -2225,7 +2224,7 @@ public class Binder<BEAN> implements Serializable { } String propertyName = descriptor.get().getName(); - if (boundProperties.contains(propertyName)) { + if (boundProperties.containsKey(propertyName)) { return; } @@ -2237,10 +2236,25 @@ public class Binder<BEAN> 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<Binding<BEAN, ?>> getBinding(String propertyName) { + return Optional.ofNullable(boundProperties.get(propertyName)); } - private Optional<BinderPropertyDefinition<BEAN, ?>> getPropertyDescriptor( + private Optional<PropertyDefinition<BEAN, ?>> getPropertyDescriptor( Field field) { PropertyId propertyIdAnnotation = field.getAnnotation(PropertyId.class); @@ -2254,8 +2268,7 @@ public class Binder<BEAN> 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/PropertyDefinition.java index b4145a8c4f..79bb2159b4 100644 --- a/server/src/main/java/com/vaadin/data/BinderPropertyDefinition.java +++ b/server/src/main/java/com/vaadin/data/PropertyDefinition.java @@ -21,17 +21,17 @@ import java.util.Optional; import com.vaadin.server.Setter; /** - * A property from a {@link BinderPropertySet}. + * A property from a {@link PropertySet}. * * @author Vaadin Ltd * @since * * @param <T> - * the type of the binder property set + * the type of the property set * @param <V> * the property type */ -public interface BinderPropertyDefinition<T, V> extends Serializable { +public interface PropertyDefinition<T, V> extends Serializable { /** * Gets the value provider that is used for finding the value of this * property for a bean. @@ -62,9 +62,16 @@ public interface BinderPropertyDefinition<T, V> extends Serializable { public String getName(); /** - * Gets the {@link BinderPropertySet} that this property belongs to. + * Gets the human readable caption to show for this property. * - * @return the binder property set, not <code>null</code> + * @return the caption to show, not <code>null</code> */ - public BinderPropertySet<T> getPropertySet(); + public String getCaption(); + + /** + * Gets the {@link PropertySet} that this property belongs to. + * + * @return the property set, not <code>null</code> + */ + public PropertySet<T> getPropertySet(); } diff --git a/server/src/main/java/com/vaadin/data/BinderPropertySet.java b/server/src/main/java/com/vaadin/data/PropertySet.java index 6252a228aa..7b557dc293 100644 --- a/server/src/main/java/com/vaadin/data/BinderPropertySet.java +++ b/server/src/main/java/com/vaadin/data/PropertySet.java @@ -20,7 +20,8 @@ import java.util.Optional; import java.util.stream.Stream; /** - * Describes a set of properties that can be used with a {@link Binder}. + * Describes a set of properties that can be used for configuration based on + * property names instead of setter and getter callbacks. * * @author Vaadin Ltd * @@ -29,13 +30,13 @@ import java.util.stream.Stream; * @param <T> * the type for which the properties are defined */ -public interface BinderPropertySet<T> extends Serializable { +public interface PropertySet<T> extends Serializable { /** * Gets all known properties as a stream. * * @return a stream of property names, not <code>null</code> */ - public Stream<BinderPropertyDefinition<T, ?>> getProperties(); + public Stream<PropertyDefinition<T, ?>> getProperties(); /** * Gets the definition for the named property, or an empty optional if there @@ -46,5 +47,5 @@ public interface BinderPropertySet<T> extends Serializable { * @return the property definition, or empty optional if property doesn't * exist */ - public Optional<BinderPropertyDefinition<T, ?>> getProperty(String name); + public Optional<PropertyDefinition<T, ?>> 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<T> extends AbstractListing<T> 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 <C extends HasValue<V> & Component> Column<T, V> setEditorComponent( C editorComponent, Setter<T, V> setter) { @@ -1624,6 +1629,47 @@ public class Grid<T> extends AbstractListing<T> implements HasComponents, } /** + * 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. + * <p> + * 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. + * <p> + * <strong>Note:</strong> 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 <F, C extends HasValue<F> & Component> Column<T, V> 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<T, F> binding = getGrid().getEditor().getBinder() + .bind(editorComponent, propertyName); + + return setEditorBinding(binding); + } + + /** * Gets the grid that this column belongs to. * * @return the grid that this column belongs to, or <code>null</code> if @@ -1851,10 +1897,64 @@ public class Grid<T> extends AbstractListing<T> implements HasComponents, private Editor<T> editor; + private final PropertySet<T> 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<T>() { + @Override + public Stream<PropertyDefinition<T, ?>> getProperties() { + // No columns configured by default + return Stream.empty(); + } + + @Override + public Optional<PropertyDefinition<T, ?>> 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 <code>null</code> + * @see #Grid() + * @see #withPropertySet(PropertySet) + */ + public Grid(Class<T> 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 <code>null</code>. + */ + protected Grid(PropertySet<T> propertySet) { + Objects.requireNonNull(propertySet, "propertySet cannot be null"); + this.propertySet = propertySet; + registerRpc(new GridServerRpcImpl()); setDefaultHeaderRow(appendHeaderRow()); @@ -1882,6 +1982,33 @@ public class Grid<T> extends AbstractListing<T> 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)}. + * <p> + * 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 <code>null</code>. + * @return a new grid using the provided property set, not <code>null</code> + */ + public static <BEAN> Grid<BEAN> withPropertySet( + PropertySet<BEAN> propertySet) { + return new Grid<>(propertySet); } /** @@ -1940,6 +2067,37 @@ public class Grid<T> extends AbstractListing<T> implements HasComponents, } /** + * 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. + * <p> + * This method can only be used for a <code>Grid</code> created using + * {@link Grid#Grid(Class)} or {@link #withPropertySet(PropertySet)}. + * + * @param propertyName + * the property name of the new column, not <code>null</code> + * @return the newly added column, not <code>null</code> + */ + public Column<T, ?> 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<T, ?> 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 * String using {@link Object#toString()}. Sorting in memory is executed by @@ -2070,7 +2228,8 @@ public class Grid<T> extends AbstractListing<T> 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 + * <code>null</code> if there is no such column */ public Column<T, ?> getColumn(String columnId) { return columnIds.get(columnId); @@ -2983,7 +3142,7 @@ public class Grid<T> extends AbstractListing<T> implements HasComponents, * @return editor */ protected Editor<T> 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<T> extends AbstractGridExtension<T> /** * Constructor for internal implementation of the Editor. + * + * @param propertySet + * the property set to use for configuring the default binder */ - public EditorImpl() { + public EditorImpl(PropertySet<T> propertySet) { rpc = getRpcProxy(EditorClientRpc.class); registerRpc(new EditorServerRpc() { @@ -142,7 +146,7 @@ public class EditorImpl<T> extends AbstractGridExtension<T> } }); - setBinder(new Binder<>()); + setBinder(Binder.withPropertySet(propertySet)); } @Override diff --git a/server/src/test/java/com/vaadin/data/BeanBinderPropertySetTest.java b/server/src/test/java/com/vaadin/data/BeanPropertySetTest.java index cf56dd9368..4e888846df 100644 --- a/server/src/test/java/com/vaadin/data/BeanBinderPropertySetTest.java +++ b/server/src/test/java/com/vaadin/data/BeanPropertySetTest.java @@ -32,13 +32,13 @@ import org.junit.Test; import com.vaadin.data.provider.bov.Person; import com.vaadin.tests.server.ClassesSerializableTest; -public class BeanBinderPropertySetTest { +public class BeanPropertySetTest { @Test public void testSerializeDeserialize_propertySet() throws Exception { - BinderPropertySet<Person> originalPropertySet = BeanBinderPropertySet + PropertySet<Person> originalPropertySet = BeanPropertySet .get(Person.class); - BinderPropertySet<Person> deserializedPropertySet = ClassesSerializableTest + PropertySet<Person> deserializedPropertySet = ClassesSerializableTest .serializeAndDeserialize(originalPropertySet); Assert.assertSame( @@ -49,7 +49,7 @@ public class BeanBinderPropertySetTest { @Test public void testSerializeDeserialize_propertySet_cacheCleared() throws Exception { - BinderPropertySet<Person> originalPropertySet = BeanBinderPropertySet + PropertySet<Person> originalPropertySet = BeanPropertySet .get(Person.class); ByteArrayOutputStream bs = new ByteArrayOutputStream(); @@ -59,7 +59,7 @@ public class BeanBinderPropertySetTest { // Simulate deserializing into a different JVM by clearing the instance // map - Field instancesField = BeanBinderPropertySet.class + Field instancesField = BeanPropertySet.class .getDeclaredField("instances"); instancesField.setAccessible(true); Map<?, ?> instances = (Map<?, ?>) instancesField.get(null); @@ -67,13 +67,12 @@ public class BeanBinderPropertySetTest { ObjectInputStream in = new ObjectInputStream( new ByteArrayInputStream(data)); - BinderPropertySet<Person> deserializedPropertySet = (BinderPropertySet<Person>) in + PropertySet<Person> deserializedPropertySet = (PropertySet<Person>) in .readObject(); Assert.assertSame( "Deserialized instance should be the same as in the cache", - BeanBinderPropertySet.get(Person.class), - deserializedPropertySet); + BeanPropertySet.get(Person.class), deserializedPropertySet); Assert.assertNotSame( "Deserialized instance should not be the same as the original", originalPropertySet, deserializedPropertySet); @@ -81,11 +80,11 @@ public class BeanBinderPropertySetTest { @Test public void testSerializeDeserialize_propertyDefinition() throws Exception { - BinderPropertyDefinition<Person, ?> definition = BeanBinderPropertySet + PropertyDefinition<Person, ?> definition = BeanPropertySet .get(Person.class).getProperty("born") .orElseThrow(RuntimeException::new); - BinderPropertyDefinition<Person, ?> deserializedDefinition = ClassesSerializableTest + PropertyDefinition<Person, ?> deserializedDefinition = ClassesSerializableTest .serializeAndDeserialize(definition); ValueProvider<Person, ?> getter = deserializedDefinition.getGetter(); @@ -97,19 +96,17 @@ public class BeanBinderPropertySetTest { Assert.assertSame( "Deserialized instance should be the same as in the cache", - BeanBinderPropertySet.get(Person.class).getProperty("born") + BeanPropertySet.get(Person.class).getProperty("born") .orElseThrow(RuntimeException::new), deserializedDefinition); } @Test public void properties() { - BinderPropertySet<Person> propertySet = BeanBinderPropertySet - .get(Person.class); + PropertySet<Person> propertySet = BeanPropertySet.get(Person.class); Set<String> propertyNames = propertySet.getProperties() - .map(BinderPropertyDefinition::getName) - .collect(Collectors.toSet()); + .map(PropertyDefinition::getName).collect(Collectors.toSet()); Assert.assertEquals(new HashSet<>(Arrays.asList("name", "born")), propertyNames); diff --git a/server/src/test/java/com/vaadin/data/BinderCustomPropertySetTest.java b/server/src/test/java/com/vaadin/data/BinderCustomPropertySetTest.java index d8ebf4da10..65bfb36c95 100644 --- a/server/src/test/java/com/vaadin/data/BinderCustomPropertySetTest.java +++ b/server/src/test/java/com/vaadin/data/BinderCustomPropertySetTest.java @@ -16,6 +16,7 @@ package com.vaadin.data; import java.util.HashMap; +import java.util.Locale; import java.util.Map; import java.util.Optional; import java.util.stream.Stream; @@ -28,7 +29,7 @@ import com.vaadin.ui.TextField; public class BinderCustomPropertySetTest { public static class MapPropertyDefinition - implements BinderPropertyDefinition<Map<String, String>, String> { + implements PropertyDefinition<Map<String, String>, String> { private MapPropertySet propertySet; private String name; @@ -65,26 +66,31 @@ public class BinderCustomPropertySetTest { } @Override - public BinderPropertySet<Map<String, String>> getPropertySet() { + public PropertySet<Map<String, String>> getPropertySet() { return propertySet; } + @Override + public String getCaption() { + return name.toUpperCase(Locale.ENGLISH); + } + } public static class MapPropertySet - implements BinderPropertySet<Map<String, String>> { + implements PropertySet<Map<String, String>> { @Override - public Stream<BinderPropertyDefinition<Map<String, String>, ?>> getProperties() { + public Stream<PropertyDefinition<Map<String, String>, ?>> getProperties() { return Stream.of("one", "two", "three").map(this::createProperty); } @Override - public Optional<BinderPropertyDefinition<Map<String, String>, ?>> getProperty( + public Optional<PropertyDefinition<Map<String, String>, ?>> getProperty( String name) { return Optional.of(createProperty(name)); } - private BinderPropertyDefinition<Map<String, String>, ?> createProperty( + private PropertyDefinition<Map<String, String>, ?> createProperty( String name) { return new MapPropertyDefinition(this, name); } diff --git a/server/src/test/java/com/vaadin/data/provider/bov/Person.java b/server/src/test/java/com/vaadin/data/provider/bov/Person.java index 40e2b0cb74..054cf3aeea 100644 --- a/server/src/test/java/com/vaadin/data/provider/bov/Person.java +++ b/server/src/test/java/com/vaadin/data/provider/bov/Person.java @@ -17,13 +17,8 @@ package com.vaadin.data.provider.bov; import java.io.Serializable; -/** - * POJO - * - * @author Vaadin Ltd - */ public class Person implements Serializable { - private final String name; + private String name; private final int born; public Person(String name, int born) { @@ -35,6 +30,10 @@ public class Person implements Serializable { return name; } + public void setName(String name) { + this.name = name; + } + public int getBorn() { return born; } diff --git a/server/src/test/java/com/vaadin/tests/server/component/grid/GridTest.java b/server/src/test/java/com/vaadin/tests/server/component/grid/GridTest.java index 601a63115e..fc644761bf 100644 --- a/server/src/test/java/com/vaadin/tests/server/component/grid/GridTest.java +++ b/server/src/test/java/com/vaadin/tests/server/component/grid/GridTest.java @@ -6,25 +6,37 @@ import static org.junit.Assert.assertNotNull; import java.util.ArrayList; import java.util.Arrays; +import java.util.HashSet; import java.util.List; import java.util.Optional; +import java.util.Set; import java.util.concurrent.atomic.AtomicReference; +import java.util.stream.Collectors; +import java.util.stream.Stream; import org.easymock.Capture; import org.junit.Assert; import org.junit.Before; import org.junit.Test; +import com.vaadin.data.Binder.Binding; +import com.vaadin.data.ValidationException; import com.vaadin.data.ValueProvider; import com.vaadin.data.provider.GridSortOrder; +import com.vaadin.data.provider.bov.Person; import com.vaadin.event.selection.SelectionEvent; import com.vaadin.shared.data.sort.SortDirection; import com.vaadin.shared.ui.grid.HeightMode; +import com.vaadin.tests.util.MockUI; import com.vaadin.ui.Grid; import com.vaadin.ui.Grid.Column; import com.vaadin.ui.Grid.SelectionMode; +import com.vaadin.ui.TextField; import com.vaadin.ui.renderers.NumberRenderer; +import elemental.json.Json; +import elemental.json.JsonObject; + public class GridTest { private Grid<String> grid; @@ -262,4 +274,83 @@ public class GridTest { Assert.assertEquals(0, list.size()); Assert.assertTrue(fired.get()); } + + @Test + public void beanGrid() { + Grid<Person> grid = new Grid<>(Person.class); + + Column<Person, ?> nameColumn = grid.getColumn("name"); + Column<Person, ?> bornColumn = grid.getColumn("born"); + + Assert.assertNotNull(nameColumn); + Assert.assertNotNull(bornColumn); + + Assert.assertEquals("Name", nameColumn.getCaption()); + Assert.assertEquals("Born", bornColumn.getCaption()); + + JsonObject json = getRowData(grid, new Person("Lorem", 2000)); + + Set<String> values = Stream.of(json.keys()).map(json::getString) + .collect(Collectors.toSet()); + + Assert.assertEquals(new HashSet<>(Arrays.asList("Lorem", "2000")), + values); + } + + @Test + public void beanGrid_editor() throws ValidationException { + Grid<Person> grid = new Grid<>(Person.class); + + Column<Person, ?> nameColumn = grid.getColumn("name"); + + TextField nameField = new TextField(); + nameColumn.setEditorComponent(nameField); + + Optional<Binding<Person, ?>> maybeBinding = grid.getEditor().getBinder() + .getBinding("name"); + Assert.assertTrue(maybeBinding.isPresent()); + + Binding<Person, ?> binding = maybeBinding.get(); + Assert.assertSame(nameField, binding.getField()); + + Person person = new Person("Lorem", 2000); + grid.getEditor().getBinder().setBean(person); + + Assert.assertEquals("Lorem", nameField.getValue()); + + nameField.setValue("Ipsum"); + Assert.assertEquals("Ipsum", person.getName()); + } + + @Test(expected = IllegalStateException.class) + public void oneArgSetEditor_nonBeanGrid() { + Grid<Person> grid = new Grid<>(); + Column<Person, String> nameCol = grid.addColumn(Person::getName) + .setId("name"); + + nameCol.setEditorComponent(new TextField()); + } + + @Test(expected = IllegalStateException.class) + public void addExistingColumnById_throws() { + Grid<Person> grid = new Grid<>(Person.class); + grid.addColumn("name"); + } + + private static <T> JsonObject getRowData(Grid<T> grid, T row) { + JsonObject json = Json.createObject(); + if (grid.getColumns().isEmpty()) { + return json; + } + + // generateData only works if Grid is attached + new MockUI().setContent(grid); + + grid.getColumns().forEach(column -> column.generateData(row, json)); + + // Detach again + grid.getUI().setContent(null); + + return json.getObject("d"); + } } |