diff options
author | Olli Tietäväinen <ollit@vaadin.com> | 2018-12-28 09:25:11 +0200 |
---|---|---|
committer | Sun Zhe <31067185+ZheSun88@users.noreply.github.com> | 2018-12-28 09:25:11 +0200 |
commit | 613f36ff23fe06c676f9a71e92c04ccabcd19c63 (patch) | |
tree | 8a7ad51680bd9889bdd7897e69a41a00f724d962 | |
parent | d9de8a9470d0e89590e8fd77e1471c5154e110cd (diff) | |
download | vaadin-framework-613f36ff23fe06c676f9a71e92c04ccabcd19c63.tar.gz vaadin-framework-613f36ff23fe06c676f9a71e92c04ccabcd19c63.zip |
Grid column npe behavior (#11390)
* add possibility to configure nested null behavior to Grid.Column
* added addColumn method that allows configuring the nested null handling behavior of the column
* added uitest for nested null value handling, implements #11137
3 files changed, 253 insertions, 3 deletions
diff --git a/server/src/main/java/com/vaadin/ui/Grid.java b/server/src/main/java/com/vaadin/ui/Grid.java index 7c3467eaa3..81f46f8d59 100644 --- a/server/src/main/java/com/vaadin/ui/Grid.java +++ b/server/src/main/java/com/vaadin/ui/Grid.java @@ -845,6 +845,23 @@ public class Grid<T> extends AbstractListing<T> implements HasComponents, */ public static class Column<T, V> extends AbstractExtension { + /** + * behavior when parsing nested properties which may contain + * <code>null</code> values in the property chain + */ + public enum NestedNullBehavior { + /** + * throw a NullPointerException if there is a nested + * <code>null</code> value + */ + THROW, + /** + * silently ignore any exceptions caused by nested <code>null</code> + * values + */ + ALLOW_NULLS + } + private final ValueProvider<T, V> valueProvider; private ValueProvider<V, ?> presentationProvider; @@ -856,6 +873,7 @@ public class Grid<T> extends AbstractListing<T> implements HasComponents, return Stream.of(new QuerySortOrder(id, direction)); }; + private NestedNullBehavior nestedNullBehavior = NestedNullBehavior.THROW; private boolean sortable = true; private SerializableComparator<T> comparator; private StyleGenerator<T> styleGenerator = item -> null; @@ -990,6 +1008,38 @@ public class Grid<T> extends AbstractListing<T> implements HasComponents, } } + /** + * Constructs a new Column configuration with given renderer and value + * provider. + * <p> + * For a more complete explanation on presentation provider, see + * {@link #setRenderer(ValueProvider, Renderer)}. + * + * @param valueProvider + * the function to get values from items, not + * <code>null</code> + * @param presentationProvider + * the function to get presentations from the value of this + * column, not <code>null</code>. For more details, see + * {@link #setRenderer(ValueProvider, Renderer)} + * @param nestedNullBehavior + * behavior on encountering nested <code>null</code> values + * when reading the value from the bean + * @param renderer + * the presentation renderer, not <code>null</code> + * @param <P> + * the presentation type + * + * @since + */ + protected <P> Column(ValueProvider<T, V> valueProvider, + ValueProvider<V, P> presentationProvider, + Renderer<? super P> renderer, + NestedNullBehavior nestedNullBehavior) { + this(valueProvider, presentationProvider, renderer); + this.nestedNullBehavior = nestedNullBehavior; + } + private static int compareMaybeComparables(Object a, Object b) { if (hasCommonComparableBaseType(a, b)) { return compareComparables(a, b); @@ -1053,8 +1103,16 @@ public class Grid<T> extends AbstractListing<T> implements HasComponents, @SuppressWarnings("unchecked") private <P> JsonValue generateRendererValue(T item, ValueProvider<V, P> presentationProvider, Connector renderer) { - P presentationValue = presentationProvider - .apply(valueProvider.apply(item)); + V value; + try { + value = valueProvider.apply(item); + } catch (NullPointerException npe) { + value = null; + if (NestedNullBehavior.THROW == nestedNullBehavior) { + throw npe; + } + } + P presentationValue = presentationProvider.apply(value); // Make Grid track components. if (renderer instanceof ComponentRenderer @@ -2723,6 +2781,59 @@ public class Grid<T> extends AbstractListing<T> implements HasComponents, } /** + * Adds a new column with the given property name and renderer. 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)}. + * <p> + * You can add columns for nested properties with dot notation, eg. + * <code>"property.nestedProperty"</code> + * + * @param propertyName + * the property name of the new column, not <code>null</code> + * @param renderer + * the renderer to use, not <code>null</code> + * @param nestedNullBehavior + * the behavior when + * @return the newly added column, not <code>null</code> + */ + public Column<T, ?> addColumn(String propertyName, + AbstractRenderer<? super T, ?> renderer, + Column.NestedNullBehavior nestedNullBehavior) { + Objects.requireNonNull(propertyName, "Property name cannot be null"); + Objects.requireNonNull(renderer, "Renderer 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)); + + if (!renderer.getPresentationType() + .isAssignableFrom(definition.getType())) { + throw new IllegalArgumentException( + renderer + " cannot be used with a property of type " + + definition.getType().getName()); + } + @SuppressWarnings({ "unchecked", "rawtypes" }) + Column<T, ?> column = createColumn(definition.getGetter(), + ValueProvider.identity(), (AbstractRenderer) renderer, + nestedNullBehavior); + String generatedIdentifier = getGeneratedIdentifier(); + addColumn(generatedIdentifier, column); + column.setId(definition.getName()).setCaption(definition.getCaption()); + return column; + } + + /** * 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()}. In-memory sorting will use the @@ -2824,7 +2935,7 @@ public class Grid<T> extends AbstractListing<T> implements HasComponents, /** * Adds a column that shows components. * <p> - * This is a shorthand for {@link #addColum()} with a + * This is a shorthand for {@link #addColumn()} with a * {@link ComponentRenderer}. * * @param componentProvider @@ -2865,6 +2976,34 @@ public class Grid<T> extends AbstractListing<T> implements HasComponents, return new Column<>(valueProvider, presentationProvider, renderer); } + /** + * Creates a column instance from a value provider, presentation provider + * and a renderer. + * + * @param valueProvider + * the value provider + * @param presentationProvider + * the presentation provider + * @param renderer + * the renderer + * @param nestedNullBehavior + * the behavior when facing nested <code>null</code> values + * @return a new column instance + * @param <V> + * the column value type + * @param <P> + * the column presentation type + * + * @since + */ + private <V, P> Column<T, V> createColumn(ValueProvider<T, V> valueProvider, + ValueProvider<V, P> presentationProvider, + AbstractRenderer<? super T, ? super P> renderer, + Column.NestedNullBehavior nestedNullBehavior) { + return new Column<>(valueProvider, presentationProvider, renderer, + nestedNullBehavior); + } + private void addColumn(String identifier, Column<T, ?> column) { if (getColumns().contains(column)) { return; diff --git a/uitest/src/main/java/com/vaadin/tests/components/grid/GridNullSafeNestedPropertyColumn.java b/uitest/src/main/java/com/vaadin/tests/components/grid/GridNullSafeNestedPropertyColumn.java new file mode 100644 index 0000000000..888296d5f8 --- /dev/null +++ b/uitest/src/main/java/com/vaadin/tests/components/grid/GridNullSafeNestedPropertyColumn.java @@ -0,0 +1,65 @@ +package com.vaadin.tests.components.grid; + +import com.vaadin.annotations.Widgetset; +import com.vaadin.data.provider.ListDataProvider; +import com.vaadin.server.VaadinRequest; +import com.vaadin.tests.components.AbstractTestUI; +import com.vaadin.tests.data.bean.Address; +import com.vaadin.tests.data.bean.Country; +import com.vaadin.tests.data.bean.Person; +import com.vaadin.tests.data.bean.Sex; +import com.vaadin.ui.Button; +import com.vaadin.ui.Grid; +import com.vaadin.ui.TextField; +import com.vaadin.ui.renderers.TextRenderer; + +import java.util.ArrayList; +import java.util.List; + +@Widgetset("com.vaadin.DefaultWidgetSet") +public class GridNullSafeNestedPropertyColumn extends AbstractTestUI { + + private List<Person> personList = new ArrayList<>(); + private ListDataProvider<Person> listDataProvider; + private Grid.Column nullSafeColumn = null; + private Grid.Column regularColumn = null; + + @Override + protected void setup(VaadinRequest request) { + Grid<Person> grid = new Grid<>(Person.class); + grid.setSizeFull(); + + personList.add(new Person("person", "with", "an address", 0, + Sex.UNKNOWN, new Address("street", 0, "", Country.FINLAND))); + listDataProvider = new ListDataProvider<>(personList); + grid.setDataProvider(listDataProvider); + + Button addPersonButton = new Button("add person with a null address", + event -> { + Address address = null; + Person person = new Person("person", "without", "address", + 42, Sex.UNKNOWN, address); + personList.add(person); + listDataProvider.refreshAll(); + }); + addPersonButton.setId("add"); + + Button addSafeColumnButton = new Button( + "add 'address.streetAddress' as a null-safe column", event -> { + nullSafeColumn = grid.addColumn("address.streetAddress", + new TextRenderer(), + Grid.Column.NestedNullBehavior.ALLOW_NULLS); + }); + addSafeColumnButton.setId("safe"); + + Button addUnsafeColumnButton = new Button( + "add 'address.streetAddress' column without nested null safety", + event -> { + regularColumn = grid.addColumn("address.streetAddress"); + }); + addUnsafeColumnButton.setId("unsafe"); + + addComponents(grid, addPersonButton, addSafeColumnButton, + addUnsafeColumnButton); + } +} diff --git a/uitest/src/test/java/com/vaadin/tests/components/grid/GridNullSafeNestedPropertyColumnTest.java b/uitest/src/test/java/com/vaadin/tests/components/grid/GridNullSafeNestedPropertyColumnTest.java new file mode 100644 index 0000000000..2ac67b7fc2 --- /dev/null +++ b/uitest/src/test/java/com/vaadin/tests/components/grid/GridNullSafeNestedPropertyColumnTest.java @@ -0,0 +1,46 @@ +package com.vaadin.tests.components.grid; + +import com.vaadin.testbench.By; +import com.vaadin.testbench.elements.ButtonElement; +import com.vaadin.testbench.parallel.TestCategory; +import com.vaadin.tests.tb3.MultiBrowserTest; +import org.junit.Test; +import org.openqa.selenium.WebElement; + +import java.util.List; + +import static junit.framework.TestCase.assertTrue; +import static org.junit.Assert.assertFalse; + +/** + * Tests that using a nested property name with a null bean child property won't + * cause an exception. + */ +@TestCategory("grid") +public class GridNullSafeNestedPropertyColumnTest extends MultiBrowserTest { + + @Test + public void testNullNestedPropertyInSafeGridColumn() { + openTestURL(); + waitForElementPresent(org.openqa.selenium.By.className("v-grid")); + $(ButtonElement.class).id("safe").click(); + $(ButtonElement.class).id("add").click(); + List<WebElement> errorIndicator = findElements( + By.className("v-errorindicator")); + assertTrue(errorIndicator.isEmpty()); + } + + @Test + public void testNullNestedPropertyInUnsafeGridColumn() { + openTestURL(); + waitForElementPresent(org.openqa.selenium.By.className("v-grid")); + $(ButtonElement.class).id("unsafe").click(); + $(ButtonElement.class).id("add").click(); + List<WebElement> errorIndicator = findElements( + By.className("v-errorindicator")); + assertFalse( + "There should be an error indicator when adding nested null values to Grid", + errorIndicator.isEmpty()); + } + +} |