Browse Source

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
tags/8.0.0.beta2
Leif Åstrand 7 years ago
parent
commit
953e7212d8

+ 24
- 12
documentation/components/components-grid.asciidoc View File

@@ -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.

server/src/main/java/com/vaadin/data/BeanBinderPropertySet.java → server/src/main/java/com/vaadin/data/BeanPropertySet.java View File

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


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

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

+ 45
- 32
server/src/main/java/com/vaadin/data/Binder.java View File

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

server/src/main/java/com/vaadin/data/BinderPropertyDefinition.java → server/src/main/java/com/vaadin/data/PropertyDefinition.java View File

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

server/src/main/java/com/vaadin/data/BinderPropertySet.java → server/src/main/java/com/vaadin/data/PropertySet.java View File

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

+ 162
- 3
server/src/main/java/com/vaadin/ui/Grid.java View File

@@ -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) {
@@ -1623,6 +1628,47 @@ public class Grid<T> extends AbstractListing<T> 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.
* <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.
*
@@ -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);
}

/**
@@ -1939,6 +2066,37 @@ public class Grid<T> extends AbstractListing<T> 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.
* <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
@@ -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) {

+ 6
- 2
server/src/main/java/com/vaadin/ui/components/grid/EditorImpl.java View File

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

server/src/test/java/com/vaadin/data/BeanBinderPropertySetTest.java → server/src/test/java/com/vaadin/data/BeanPropertySetTest.java View File

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

+ 12
- 6
server/src/test/java/com/vaadin/data/BinderCustomPropertySetTest.java View File

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

+ 5
- 6
server/src/test/java/com/vaadin/data/provider/bov/Person.java View File

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

+ 91
- 0
server/src/test/java/com/vaadin/tests/server/component/grid/GridTest.java View File

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

Loading…
Cancel
Save