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

[[components.grid.data]] [[components.grid.data]]
== Binding to 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 described in
<<dummy/../../../framework/datamodel/datamodel-providers.asciidoc#datamodel.dataproviders,"Showing Many Items in a Listing">>. <<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 By default, it is bound to List of items. You can set the items with the
grid.addSelectionListener(event -> { grid.addSelectionListener(event -> {
Set<Person> selected = event.getAllSelectedItems(); Set<Person> selected = event.getAllSelectedItems();
Notification.show(selected.size() + " items selected"); Notification.show(selected.size() + " items selected");
}
});
---- ----


Programmatically selecting the value is possible via [methodname]#select(T)#. Programmatically selecting the value is possible via [methodname]#select(T)#.
// Allow deleting only if there's any selected // Allow deleting only if there's any selected
deleteSelected.setEnabled( deleteSelected.setEnabled(
event.getNewSelection().size() > 0); event.getNewSelection().size() > 0);
};
});
---- ----




[source, java] [source, java]
---- ----
grid.addCellClickListener(event -> grid.addCellClickListener(event ->
Notification.show("Value: " + event.getItem());
Notification.show("Value: " + event.getItem()));
---- ----


The clicked grid cell is also automatically focused. The clicked grid cell is also automatically focused.
[[components.grid.columns]] [[components.grid.columns]]
== Configuring 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] [source, java]
---- ----


In the following, we describe the basic column configuration. 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]] [[components.grid.columns.order]]
=== Column Order === Column Order


new ButtonRenderer(clickEvent -> { new ButtonRenderer(clickEvent -> {
people.remove(clickEvent.getValue()); people.remove(clickEvent.getValue());
grid.setItems(people); grid.setItems(people);
});
}));
---- ----


[classname]#ImageRenderer#:: Renders the cell as an image. [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



import com.vaadin.data.util.BeanUtil; import com.vaadin.data.util.BeanUtil;
import com.vaadin.server.Setter; import com.vaadin.server.Setter;
import com.vaadin.shared.util.SharedUtil;
import com.vaadin.util.ReflectTools; 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 * @author Vaadin Ltd
* *
* @param <T> * @param <T>
* the type of the bean * 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 * Serialized form of a property set. When deserialized, the property set
* existing cached instance or creates a new one. * existing cached instance or creates a new one.
* *
* @see #readResolve() * @see #readResolve()
* @see BeanBinderPropertyDefinition#writeReplace()
* @see BeanPropertyDefinition#writeReplace()
*/ */
private static class SerializedPropertySet implements Serializable { private static class SerializedPropertySet implements Serializable {
private final Class<?> beanType; private final Class<?> beanType;
* definition is then fetched from the property set. * definition is then fetched from the property set.
* *
* @see #readResolve() * @see #readResolve()
* @see BeanBinderPropertySet#writeReplace()
* @see BeanPropertySet#writeReplace()
*/ */
private static class SerializedPropertyDefinition implements Serializable { private static class SerializedPropertyDefinition implements Serializable {
private final Class<?> beanType; private final Class<?> beanType;
} }
} }


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 PropertyDescriptor descriptor;
private final BeanBinderPropertySet<T> propertySet;
private final BeanPropertySet<T> propertySet;


public BeanBinderPropertyDefinition(
BeanBinderPropertySet<T> propertySet,
public BeanPropertyDefinition(BeanPropertySet<T> propertySet,
PropertyDescriptor descriptor) { PropertyDescriptor descriptor) {
this.propertySet = propertySet; this.propertySet = propertySet;
this.descriptor = descriptor; this.descriptor = descriptor;
} }


@Override @Override
public BeanBinderPropertySet<T> getPropertySet() {
public String getCaption() {
return SharedUtil.propertyIdToHumanFriendly(getName());
}

@Override
public BeanPropertySet<T> getPropertySet() {
return propertySet; return propertySet;
} }


} }
} }


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 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; this.beanType = beanType;


try { try {
definitions = BeanUtil.getBeanPropertyDescriptors(beanType).stream() definitions = BeanUtil.getBeanPropertyDescriptors(beanType).stream()
.filter(BeanBinderPropertySet::hasNonObjectReadMethod)
.map(descriptor -> new BeanBinderPropertyDefinition<>(this,
.filter(BeanPropertySet::hasNonObjectReadMethod)
.map(descriptor -> new BeanPropertyDefinition<>(this,
descriptor)) descriptor))
.collect(Collectors.toMap(BinderPropertyDefinition::getName,
.collect(Collectors.toMap(PropertyDefinition::getName,
Function.identity())); Function.identity()));
} catch (IntrospectionException e) { } catch (IntrospectionException e) {
throw new IllegalArgumentException( throw new IllegalArgumentException(
} }


/** /**
* Gets a {@link BeanBinderPropertySet} for the given bean type.
* Gets a {@link BeanPropertySet} for the given bean type.
* *
* @param beanType * @param beanType
* the bean type to get a property set for, not <code>null</code> * 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") @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"); Objects.requireNonNull(beanType, "Bean type cannot be null");


// Cache the reflection results // Cache the reflection results
return (BinderPropertySet<T>) instances.computeIfAbsent(beanType,
BeanBinderPropertySet::new);
return (PropertySet<T>) instances.computeIfAbsent(beanType,
BeanPropertySet::new);
} }


@Override @Override
public Stream<BinderPropertyDefinition<T, ?>> getProperties() {
public Stream<PropertyDefinition<T, ?>> getProperties() {
return definitions.values().stream(); return definitions.values().stream();
} }


@Override @Override
public Optional<BinderPropertyDefinition<T, ?>> getProperty(String name) {
public Optional<PropertyDefinition<T, ?>> getProperty(String name) {
return Optional.ofNullable(definitions.get(name)); return Optional.ofNullable(definitions.get(name));
} }



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

@Override @Override
protected BindingBuilder<BEAN, ?> configureBinding( protected BindingBuilder<BEAN, ?> configureBinding(
BindingBuilder<BEAN, ?> binding, BindingBuilder<BEAN, ?> binding,
BinderPropertyDefinition<BEAN, ?> definition) {
PropertyDefinition<BEAN, ?> definition) {
return binding.withValidator( return binding.withValidator(
new BeanValidator(beanType, definition.getName())); new BeanValidator(beanType, definition.getName()));
} }

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

import java.util.Arrays; import java.util.Arrays;
import java.util.Collections; import java.util.Collections;
import java.util.HashMap; import java.util.HashMap;
import java.util.HashSet;
import java.util.IdentityHashMap; import java.util.IdentityHashMap;
import java.util.LinkedHashSet; import java.util.LinkedHashSet;
import java.util.List; import java.util.List;
/** /**
* Completes this binding by connecting the field to the property with * Completes this binding by connecting the field to the property with
* the given name. The getter and setter of the property are looked up * the given name. The getter and setter of the property are looked up
* using a {@link BinderPropertySet}.
* using a {@link PropertySet}.
* <p> * <p>
* For a <code>Binder</code> created using the * For a <code>Binder</code> created using the
* {@link Binder#Binder(Class)} constructor, introspection will be used * {@link Binder#Binder(Class)} constructor, introspection will be used
* if the property has no accessible getter * if the property has no accessible getter
* @throws IllegalStateException * @throws IllegalStateException
* if the binder is not configured with an appropriate * if the binder is not configured with an appropriate
* {@link BinderPropertySet}
* {@link PropertySet}
* *
* @see Binder.BindingBuilder#bind(ValueProvider, Setter) * @see Binder.BindingBuilder#bind(ValueProvider, Setter)
*/ */
"Property name cannot be null"); "Property name cannot be null");
checkUnbound(); checkUnbound();


BinderPropertyDefinition<BEAN, ?> definition = getBinder().propertySet
PropertyDefinition<BEAN, ?> definition = getBinder().propertySet
.getProperty(propertyName) .getProperty(propertyName)
.orElseThrow(() -> new IllegalArgumentException( .orElseThrow(() -> new IllegalArgumentException(
"Could not resolve property name " + propertyName "Could not resolve property name " + propertyName
definition); definition);


try { try {
return ((BindingBuilder) finalBinding).bind(getter, setter);
Binding binding = ((BindingBuilder) finalBinding).bind(getter,
setter);
getBinder().boundProperties.put(propertyName, binding);
return binding;
} finally { } finally {
getBinder().boundProperties.add(propertyName);
getBinder().incompleteMemberFieldBindings.remove(getField()); getBinder().incompleteMemberFieldBindings.remove(getField());
} }
} }
} }
} }


private final BinderPropertySet<BEAN> propertySet;
private final PropertySet<BEAN> propertySet;


/** /**
* Property names that have been used for creating a binding. * 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<>(); private final Map<HasValue<?>, BindingBuilder<BEAN, ?>> incompleteMemberFieldBindings = new IdentityHashMap<>();


private boolean hasChanges = false; 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 #bindInstanceFields(Object)}, {@link #bind(HasValue, String)} and
* {@link BindingBuilder#bind(String)}. * {@link BindingBuilder#bind(String)}.
* *
* @param propertySet * @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"); Objects.requireNonNull(propertySet, "propertySet cannot be null");
this.propertySet = propertySet; this.propertySet = propertySet;
} }
* the bean type to use, not <code>null</code> * the bean type to use, not <code>null</code>
*/ */
public Binder(Class<BEAN> beanType) { public Binder(Class<BEAN> beanType) {
this(BeanBinderPropertySet.get(beanType));
this(BeanPropertySet.get(beanType));
} }


/** /**
* {@link #bind(HasValue, String)} or {@link BindingBuilder#bind(String)}. * {@link #bind(HasValue, String)} or {@link BindingBuilder#bind(String)}.
*/ */
public Binder() { public Binder() {
this(new BinderPropertySet<BEAN>() {
this(new PropertySet<BEAN>() {
@Override @Override
public Stream<BinderPropertyDefinition<BEAN, ?>> getProperties() {
public Stream<PropertyDefinition<BEAN, ?>> getProperties() {
throw new IllegalStateException( throw new IllegalStateException(
"A Binder created with the default constructor doesn't support listing properties."); "A Binder created with the default constructor doesn't support listing properties.");
} }


@Override @Override
public Optional<BinderPropertyDefinition<BEAN, ?>> getProperty(
public Optional<PropertyDefinition<BEAN, ?>> getProperty(
String name) { String name) {
throw new IllegalStateException( throw new IllegalStateException(
"A Binder created with the default constructor doesn't support finding properties by name."); "A Binder created with the default constructor doesn't support finding properties by name.");
} }


/** /**
* 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 #bindInstanceFields(Object)}, {@link #bind(HasValue, String)} and
* {@link BindingBuilder#bind(String)}. * {@link BindingBuilder#bind(String)}.
* <p> * <p>
* @see Binder#Binder(Class) * @see Binder#Binder(Class)
* *
* @param propertySet * @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 * @return a new binder using the provided property set, not
* <code>null</code> * <code>null</code>
*/ */
public static <BEAN> Binder<BEAN> withPropertySet( public static <BEAN> Binder<BEAN> withPropertySet(
BinderPropertySet<BEAN> propertySet) {
PropertySet<BEAN> propertySet) {
return new Binder<>(propertySet); return new Binder<>(propertySet);
} }




/** /**
* Binds the given field to the property with the given name. The getter and * 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> * <p>
* For a <code>Binder</code> created using the {@link Binder#Binder(Class)} * For a <code>Binder</code> created using the {@link Binder#Binder(Class)}
* constructor, introspection will be used to find a Java Bean property. If * constructor, introspection will be used to find a Java Bean property. If
* if the property has no accessible getter * if the property has no accessible getter
* @throws IllegalStateException * @throws IllegalStateException
* if the binder is not configured with an appropriate * if the binder is not configured with an appropriate
* {@link BinderPropertySet}
* {@link PropertySet}
* *
* @see #bind(HasValue, ValueProvider, Setter) * @see #bind(HasValue, ValueProvider, Setter)
*/ */
/** /**
* Configures the {@code binding} with the property definition * Configures the {@code binding} with the property definition
* {@code definition} before it's being bound. * {@code definition} before it's being bound.
*
*
* @param binding * @param binding
* a binding to configure * a binding to configure
* @param definition * @param definition
*/ */
protected BindingBuilder<BEAN, ?> configureBinding( protected BindingBuilder<BEAN, ?> configureBinding(
BindingBuilder<BEAN, ?> binding, BindingBuilder<BEAN, ?> binding,
BinderPropertyDefinition<BEAN, ?> definition) {
PropertyDefinition<BEAN, ?> definition) {
return binding; return binding;
} }




private void handleProperty(Field field, Object objectWithMemberFields, private void handleProperty(Field field, Object objectWithMemberFields,
BiConsumer<String, Class<?>> propertyHandler) { BiConsumer<String, Class<?>> propertyHandler) {
Optional<BinderPropertyDefinition<BEAN, ?>> descriptor = getPropertyDescriptor(
Optional<PropertyDefinition<BEAN, ?>> descriptor = getPropertyDescriptor(
field); field);


if (!descriptor.isPresent()) { if (!descriptor.isPresent()) {
} }


String propertyName = descriptor.get().getName(); String propertyName = descriptor.get().getName();
if (boundProperties.contains(propertyName)) {
if (boundProperties.containsKey(propertyName)) {
return; return;
} }


} }


propertyHandler.accept(propertyName, descriptor.get().getType()); 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) { Field field) {
PropertyId propertyIdAnnotation = field.getAnnotation(PropertyId.class); PropertyId propertyIdAnnotation = field.getAnnotation(PropertyId.class);




String minifiedFieldName = minifyFieldName(propertyId); String minifiedFieldName = minifyFieldName(propertyId);


return propertySet.getProperties()
.map(BinderPropertyDefinition::getName)
return propertySet.getProperties().map(PropertyDefinition::getName)
.filter(name -> minifyFieldName(name).equals(minifiedFieldName)) .filter(name -> minifyFieldName(name).equals(minifiedFieldName))
.findFirst().flatMap(propertySet::getProperty); .findFirst().flatMap(propertySet::getProperty);
} }

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

import com.vaadin.server.Setter; import com.vaadin.server.Setter;


/** /**
* A property from a {@link BinderPropertySet}.
* A property from a {@link PropertySet}.
* *
* @author Vaadin Ltd * @author Vaadin Ltd
* @since * @since
* *
* @param <T> * @param <T>
* the type of the binder property set
* the type of the property set
* @param <V> * @param <V>
* the property type * 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 * Gets the value provider that is used for finding the value of this
* property for a bean. * property for a bean.
public String getName(); 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

import java.util.stream.Stream; 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 * @author Vaadin Ltd
* *
* @param <T> * @param <T>
* the type for which the properties are defined * 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. * Gets all known properties as a stream.
* *
* @return a stream of property names, not <code>null</code> * @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 * Gets the definition for the named property, or an empty optional if there
* @return the property definition, or empty optional if property doesn't * @return the property definition, or empty optional if property doesn't
* exist * 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

import org.jsoup.nodes.Element; import org.jsoup.nodes.Element;
import org.jsoup.select.Elements; import org.jsoup.select.Elements;


import com.vaadin.data.BeanPropertySet;
import com.vaadin.data.Binder; import com.vaadin.data.Binder;
import com.vaadin.data.Binder.Binding; import com.vaadin.data.Binder.Binding;
import com.vaadin.data.HasDataProvider; import com.vaadin.data.HasDataProvider;
import com.vaadin.data.HasValue; import com.vaadin.data.HasValue;
import com.vaadin.data.PropertyDefinition;
import com.vaadin.data.PropertySet;
import com.vaadin.data.ValueProvider; import com.vaadin.data.ValueProvider;
import com.vaadin.data.provider.DataCommunicator; import com.vaadin.data.provider.DataCommunicator;
import com.vaadin.data.provider.DataProvider; import com.vaadin.data.provider.DataProvider;
* a setter that stores the component value in the row item * a setter that stores the component value in the row item
* @return this column * @return this column
* *
* @see #setEditorBinding(Binding)
* @see Grid#getEditor() * @see Grid#getEditor()
* @see Binder#bind(HasValue, ValueProvider, Setter)
*/ */
public <C extends HasValue<V> & Component> Column<T, V> setEditorComponent( public <C extends HasValue<V> & Component> Column<T, V> setEditorComponent(
C editorComponent, Setter<T, V> setter) { C editorComponent, Setter<T, V> setter) {
return setEditorBinding(binding); 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. * Gets the grid that this column belongs to.
* *


private Editor<T> editor; 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() { 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()); registerRpc(new GridServerRpcImpl());


setDefaultHeaderRow(appendHeaderRow()); setDefaultHeaderRow(appendHeaderRow());
} }
} }
}); });

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


/** /**
userOriginated)); 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 * 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 * column will use a {@link TextRenderer}. The value is converted to a
* *
* @param columnId * @param columnId
* the identifier of the column to get * 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) { public Column<T, ?> getColumn(String columnId) {
return columnIds.get(columnId); return columnIds.get(columnId);
* @return editor * @return editor
*/ */
protected Editor<T> createEditor() { protected Editor<T> createEditor() {
return new EditorImpl<>();
return new EditorImpl<>(propertySet);
} }


private void addExtensionComponent(Component c) { private void addExtensionComponent(Component c) {

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

import com.vaadin.data.Binder.Binding; import com.vaadin.data.Binder.Binding;
import com.vaadin.data.BinderValidationStatus; import com.vaadin.data.BinderValidationStatus;
import com.vaadin.data.BinderValidationStatusHandler; 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.EditorClientRpc;
import com.vaadin.shared.ui.grid.editor.EditorServerRpc; import com.vaadin.shared.ui.grid.editor.EditorServerRpc;
import com.vaadin.shared.ui.grid.editor.EditorState; import com.vaadin.shared.ui.grid.editor.EditorState;


/** /**
* Constructor for internal implementation of the Editor. * 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); rpc = getRpcProxy(EditorClientRpc.class);
registerRpc(new EditorServerRpc() { registerRpc(new EditorServerRpc() {


} }
}); });


setBinder(new Binder<>());
setBinder(Binder.withPropertySet(propertySet));
} }


@Override @Override

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

import com.vaadin.data.provider.bov.Person; import com.vaadin.data.provider.bov.Person;
import com.vaadin.tests.server.ClassesSerializableTest; import com.vaadin.tests.server.ClassesSerializableTest;


public class BeanBinderPropertySetTest {
public class BeanPropertySetTest {
@Test @Test
public void testSerializeDeserialize_propertySet() throws Exception { public void testSerializeDeserialize_propertySet() throws Exception {
BinderPropertySet<Person> originalPropertySet = BeanBinderPropertySet
PropertySet<Person> originalPropertySet = BeanPropertySet
.get(Person.class); .get(Person.class);


BinderPropertySet<Person> deserializedPropertySet = ClassesSerializableTest
PropertySet<Person> deserializedPropertySet = ClassesSerializableTest
.serializeAndDeserialize(originalPropertySet); .serializeAndDeserialize(originalPropertySet);


Assert.assertSame( Assert.assertSame(
@Test @Test
public void testSerializeDeserialize_propertySet_cacheCleared() public void testSerializeDeserialize_propertySet_cacheCleared()
throws Exception { throws Exception {
BinderPropertySet<Person> originalPropertySet = BeanBinderPropertySet
PropertySet<Person> originalPropertySet = BeanPropertySet
.get(Person.class); .get(Person.class);


ByteArrayOutputStream bs = new ByteArrayOutputStream(); ByteArrayOutputStream bs = new ByteArrayOutputStream();


// Simulate deserializing into a different JVM by clearing the instance // Simulate deserializing into a different JVM by clearing the instance
// map // map
Field instancesField = BeanBinderPropertySet.class
Field instancesField = BeanPropertySet.class
.getDeclaredField("instances"); .getDeclaredField("instances");
instancesField.setAccessible(true); instancesField.setAccessible(true);
Map<?, ?> instances = (Map<?, ?>) instancesField.get(null); Map<?, ?> instances = (Map<?, ?>) instancesField.get(null);


ObjectInputStream in = new ObjectInputStream( ObjectInputStream in = new ObjectInputStream(
new ByteArrayInputStream(data)); new ByteArrayInputStream(data));
BinderPropertySet<Person> deserializedPropertySet = (BinderPropertySet<Person>) in
PropertySet<Person> deserializedPropertySet = (PropertySet<Person>) in
.readObject(); .readObject();


Assert.assertSame( Assert.assertSame(
"Deserialized instance should be the same as in the cache", "Deserialized instance should be the same as in the cache",
BeanBinderPropertySet.get(Person.class),
deserializedPropertySet);
BeanPropertySet.get(Person.class), deserializedPropertySet);
Assert.assertNotSame( Assert.assertNotSame(
"Deserialized instance should not be the same as the original", "Deserialized instance should not be the same as the original",
originalPropertySet, deserializedPropertySet); originalPropertySet, deserializedPropertySet);


@Test @Test
public void testSerializeDeserialize_propertyDefinition() throws Exception { public void testSerializeDeserialize_propertyDefinition() throws Exception {
BinderPropertyDefinition<Person, ?> definition = BeanBinderPropertySet
PropertyDefinition<Person, ?> definition = BeanPropertySet
.get(Person.class).getProperty("born") .get(Person.class).getProperty("born")
.orElseThrow(RuntimeException::new); .orElseThrow(RuntimeException::new);


BinderPropertyDefinition<Person, ?> deserializedDefinition = ClassesSerializableTest
PropertyDefinition<Person, ?> deserializedDefinition = ClassesSerializableTest
.serializeAndDeserialize(definition); .serializeAndDeserialize(definition);


ValueProvider<Person, ?> getter = deserializedDefinition.getGetter(); ValueProvider<Person, ?> getter = deserializedDefinition.getGetter();


Assert.assertSame( Assert.assertSame(
"Deserialized instance should be the same as in the cache", "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), .orElseThrow(RuntimeException::new),
deserializedDefinition); deserializedDefinition);
} }


@Test @Test
public void properties() { public void properties() {
BinderPropertySet<Person> propertySet = BeanBinderPropertySet
.get(Person.class);
PropertySet<Person> propertySet = BeanPropertySet.get(Person.class);


Set<String> propertyNames = propertySet.getProperties() 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")), Assert.assertEquals(new HashSet<>(Arrays.asList("name", "born")),
propertyNames); propertyNames);

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

package com.vaadin.data; package com.vaadin.data;


import java.util.HashMap; import java.util.HashMap;
import java.util.Locale;
import java.util.Map; import java.util.Map;
import java.util.Optional; import java.util.Optional;
import java.util.stream.Stream; import java.util.stream.Stream;


public class BinderCustomPropertySetTest { public class BinderCustomPropertySetTest {
public static class MapPropertyDefinition public static class MapPropertyDefinition
implements BinderPropertyDefinition<Map<String, String>, String> {
implements PropertyDefinition<Map<String, String>, String> {


private MapPropertySet propertySet; private MapPropertySet propertySet;
private String name; private String name;
} }


@Override @Override
public BinderPropertySet<Map<String, String>> getPropertySet() {
public PropertySet<Map<String, String>> getPropertySet() {
return propertySet; return propertySet;
} }


@Override
public String getCaption() {
return name.toUpperCase(Locale.ENGLISH);
}

} }


public static class MapPropertySet public static class MapPropertySet
implements BinderPropertySet<Map<String, String>> {
implements PropertySet<Map<String, String>> {
@Override @Override
public Stream<BinderPropertyDefinition<Map<String, String>, ?>> getProperties() {
public Stream<PropertyDefinition<Map<String, String>, ?>> getProperties() {
return Stream.of("one", "two", "three").map(this::createProperty); return Stream.of("one", "two", "three").map(this::createProperty);
} }


@Override @Override
public Optional<BinderPropertyDefinition<Map<String, String>, ?>> getProperty(
public Optional<PropertyDefinition<Map<String, String>, ?>> getProperty(
String name) { String name) {
return Optional.of(createProperty(name)); return Optional.of(createProperty(name));
} }


private BinderPropertyDefinition<Map<String, String>, ?> createProperty(
private PropertyDefinition<Map<String, String>, ?> createProperty(
String name) { String name) {
return new MapPropertyDefinition(this, name); return new MapPropertyDefinition(this, name);
} }

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



import java.io.Serializable; import java.io.Serializable;


/**
* POJO
*
* @author Vaadin Ltd
*/
public class Person implements Serializable { public class Person implements Serializable {
private final String name;
private String name;
private final int born; private final int born;


public Person(String name, int born) { public Person(String name, int born) {
return name; return name;
} }


public void setName(String name) {
this.name = name;
}

public int getBorn() { public int getBorn() {
return born; return born;
} }

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



import java.util.ArrayList; import java.util.ArrayList;
import java.util.Arrays; import java.util.Arrays;
import java.util.HashSet;
import java.util.List; import java.util.List;
import java.util.Optional; import java.util.Optional;
import java.util.Set;
import java.util.concurrent.atomic.AtomicReference; import java.util.concurrent.atomic.AtomicReference;
import java.util.stream.Collectors;
import java.util.stream.Stream;


import org.easymock.Capture; import org.easymock.Capture;
import org.junit.Assert; import org.junit.Assert;
import org.junit.Before; import org.junit.Before;
import org.junit.Test; import org.junit.Test;


import com.vaadin.data.Binder.Binding;
import com.vaadin.data.ValidationException;
import com.vaadin.data.ValueProvider; import com.vaadin.data.ValueProvider;
import com.vaadin.data.provider.GridSortOrder; import com.vaadin.data.provider.GridSortOrder;
import com.vaadin.data.provider.bov.Person;
import com.vaadin.event.selection.SelectionEvent; import com.vaadin.event.selection.SelectionEvent;
import com.vaadin.shared.data.sort.SortDirection; import com.vaadin.shared.data.sort.SortDirection;
import com.vaadin.shared.ui.grid.HeightMode; import com.vaadin.shared.ui.grid.HeightMode;
import com.vaadin.tests.util.MockUI;
import com.vaadin.ui.Grid; import com.vaadin.ui.Grid;
import com.vaadin.ui.Grid.Column; import com.vaadin.ui.Grid.Column;
import com.vaadin.ui.Grid.SelectionMode; import com.vaadin.ui.Grid.SelectionMode;
import com.vaadin.ui.TextField;
import com.vaadin.ui.renderers.NumberRenderer; import com.vaadin.ui.renderers.NumberRenderer;


import elemental.json.Json;
import elemental.json.JsonObject;

public class GridTest { public class GridTest {


private Grid<String> grid; private Grid<String> grid;
Assert.assertEquals(0, list.size()); Assert.assertEquals(0, list.size());
Assert.assertTrue(fired.get()); 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