aboutsummaryrefslogtreecommitdiffstats
path: root/compatibility-server
diff options
context:
space:
mode:
Diffstat (limited to 'compatibility-server')
-rw-r--r--compatibility-server/pom.xml19
-rw-r--r--compatibility-server/src/main/java/com/vaadin/data/fieldgroup/BeanFieldGroup.java272
-rw-r--r--compatibility-server/src/main/java/com/vaadin/data/fieldgroup/Caption.java27
-rw-r--r--compatibility-server/src/main/java/com/vaadin/data/fieldgroup/DefaultFieldGroupFieldFactory.java254
-rw-r--r--compatibility-server/src/main/java/com/vaadin/data/fieldgroup/FieldGroup.java1258
-rw-r--r--compatibility-server/src/main/java/com/vaadin/data/fieldgroup/FieldGroupFieldFactory.java43
-rw-r--r--compatibility-server/src/main/java/com/vaadin/data/fieldgroup/PropertyId.java60
-rw-r--r--compatibility-server/src/main/java/com/vaadin/server/communication/data/DataGenerator.java58
-rw-r--r--compatibility-server/src/main/java/com/vaadin/server/communication/data/RpcDataProviderExtension.java632
-rw-r--r--compatibility-server/src/main/java/com/vaadin/ui/LegacyGrid.java7352
-rw-r--r--compatibility-server/src/main/java/com/vaadin/ui/renderers/AbstractJavaScriptRenderer.java175
-rw-r--r--compatibility-server/src/main/java/com/vaadin/ui/renderers/ButtonRenderer.java76
-rw-r--r--compatibility-server/src/main/java/com/vaadin/ui/renderers/ClickableRenderer.java143
-rw-r--r--compatibility-server/src/main/java/com/vaadin/ui/renderers/DateRenderer.java240
-rw-r--r--compatibility-server/src/main/java/com/vaadin/ui/renderers/HtmlRenderer.java49
-rw-r--r--compatibility-server/src/main/java/com/vaadin/ui/renderers/ImageRenderer.java68
-rw-r--r--compatibility-server/src/main/java/com/vaadin/ui/renderers/NumberRenderer.java207
-rw-r--r--compatibility-server/src/main/java/com/vaadin/ui/renderers/ProgressBarRenderer.java46
-rw-r--r--compatibility-server/src/main/java/com/vaadin/ui/renderers/Renderer.java69
-rw-r--r--compatibility-server/src/main/java/com/vaadin/ui/renderers/TextRenderer.java50
-rw-r--r--compatibility-server/src/test/java/com/vaadin/data/fieldgroup/BeanFieldGroupTest.java70
-rw-r--r--compatibility-server/src/test/java/com/vaadin/data/fieldgroup/DefaultFieldGroupFieldFactoryTest.java125
-rw-r--r--compatibility-server/src/test/java/com/vaadin/data/fieldgroup/FieldGroupDateTest.java97
-rw-r--r--compatibility-server/src/test/java/com/vaadin/data/fieldgroup/FieldGroupExceptionTest.java33
-rw-r--r--compatibility-server/src/test/java/com/vaadin/data/fieldgroup/FieldGroupTest.java96
-rw-r--r--compatibility-server/src/test/java/com/vaadin/data/util/AbstractBeanContainerTestBase.java77
-rw-r--r--compatibility-server/src/test/java/com/vaadin/data/util/AbstractContainerTestBase.java866
-rw-r--r--compatibility-server/src/test/java/com/vaadin/data/util/AbstractHierarchicalContainerTestBase.java289
-rw-r--r--compatibility-server/src/test/java/com/vaadin/data/util/AbstractInMemoryContainerTestBase.java6
-rw-r--r--compatibility-server/src/test/java/com/vaadin/data/util/BeanContainerTest.java516
-rw-r--r--compatibility-server/src/test/java/com/vaadin/data/util/BeanItemContainerGenerator.java150
-rw-r--r--compatibility-server/src/test/java/com/vaadin/data/util/BeanItemContainerSortTest.java166
-rw-r--r--compatibility-server/src/test/java/com/vaadin/data/util/BeanItemContainerTest.java997
-rw-r--r--compatibility-server/src/test/java/com/vaadin/data/util/BeanItemTest.java389
-rw-r--r--compatibility-server/src/test/java/com/vaadin/data/util/ContainerHierarchicalWrapperTest.java26
-rw-r--r--compatibility-server/src/test/java/com/vaadin/data/util/ContainerOrderedWrapperTest.java107
-rw-r--r--compatibility-server/src/test/java/com/vaadin/data/util/ContainerSizeAssertTest.java59
-rw-r--r--compatibility-server/src/test/java/com/vaadin/data/util/ContainerSortingTest.java222
-rw-r--r--compatibility-server/src/test/java/com/vaadin/data/util/FileSystemContainerTest.java16
-rw-r--r--compatibility-server/src/test/java/com/vaadin/data/util/GeneratedPropertyContainerBasicTest.java596
-rw-r--r--compatibility-server/src/test/java/com/vaadin/data/util/GeneratedPropertyContainerTest.java318
-rw-r--r--compatibility-server/src/test/java/com/vaadin/data/util/HierarchicalContainerOrderedWrapperTest.java33
-rw-r--r--compatibility-server/src/test/java/com/vaadin/data/util/HierarchicalContainerTest.java286
-rw-r--r--compatibility-server/src/test/java/com/vaadin/data/util/IndexedContainerTest.java533
-rw-r--r--compatibility-server/src/test/java/com/vaadin/data/util/MethodPropertyMemoryConsumptionTest.java150
-rw-r--r--compatibility-server/src/test/java/com/vaadin/data/util/NestedMethodPropertyTest.java351
-rw-r--r--compatibility-server/src/test/java/com/vaadin/data/util/ObjectPropertyTest.java102
-rw-r--r--compatibility-server/src/test/java/com/vaadin/data/util/PerformanceTestIndexedContainerTest.java120
-rw-r--r--compatibility-server/src/test/java/com/vaadin/data/util/PropertyDescriptorTest.java84
-rw-r--r--compatibility-server/src/test/java/com/vaadin/data/util/PropertySetItemTest.java432
-rw-r--r--compatibility-server/src/test/java/com/vaadin/data/util/ReflectToolsGetSuperFieldTest.java35
-rw-r--r--compatibility-server/src/test/java/com/vaadin/data/util/TransactionalPropertyWrapperTest.java117
-rw-r--r--compatibility-server/src/test/java/com/vaadin/tests/server/ContextClickListenerTest.java163
-rw-r--r--compatibility-server/src/test/java/com/vaadin/tests/server/component/fieldgroup/BeanFieldGroupTest.java172
-rw-r--r--compatibility-server/src/test/java/com/vaadin/tests/server/component/fieldgroup/CaseInsensitiveBindingTest.java85
-rw-r--r--compatibility-server/src/test/java/com/vaadin/tests/server/component/fieldgroup/FieldGroupTest.java149
-rw-r--r--compatibility-server/src/test/java/com/vaadin/tests/server/component/fieldgroup/FieldGroupWithReadOnlyPropertiesTest.java51
-rw-r--r--compatibility-server/src/test/java/com/vaadin/tests/server/component/fieldgroup/FieldNamedDescriptionTest.java53
-rw-r--r--compatibility-server/src/test/java/com/vaadin/tests/server/component/grid/GridAddRowBuiltinContainerTest.java219
-rw-r--r--compatibility-server/src/test/java/com/vaadin/tests/server/component/grid/GridChildrenTest.java59
-rw-r--r--compatibility-server/src/test/java/com/vaadin/tests/server/component/grid/GridColumnAddingAndRemovingTest.java134
-rw-r--r--compatibility-server/src/test/java/com/vaadin/tests/server/component/grid/GridColumnsTest.java413
-rw-r--r--compatibility-server/src/test/java/com/vaadin/tests/server/component/grid/GridContainerNotSortableTest.java103
-rw-r--r--compatibility-server/src/test/java/com/vaadin/tests/server/component/grid/GridContainerTest.java159
-rw-r--r--compatibility-server/src/test/java/com/vaadin/tests/server/component/grid/GridEditorTest.java296
-rw-r--r--compatibility-server/src/test/java/com/vaadin/tests/server/component/grid/GridExtensionTest.java41
-rw-r--r--compatibility-server/src/test/java/com/vaadin/tests/server/component/grid/GridSelectionTest.java377
-rw-r--r--compatibility-server/src/test/java/com/vaadin/tests/server/component/grid/GridStateTest.java44
-rw-r--r--compatibility-server/src/test/java/com/vaadin/tests/server/component/grid/GridStaticSectionTest.java132
-rw-r--r--compatibility-server/src/test/java/com/vaadin/tests/server/component/grid/MultiSelectionModelTest.java171
-rw-r--r--compatibility-server/src/test/java/com/vaadin/tests/server/component/grid/SingleSelectionModelTest.java153
-rw-r--r--compatibility-server/src/test/java/com/vaadin/tests/server/component/grid/TestGrid.java62
-rw-r--r--compatibility-server/src/test/java/com/vaadin/tests/server/component/grid/declarative/GridColumnDeclarativeTest.java101
-rw-r--r--compatibility-server/src/test/java/com/vaadin/tests/server/component/grid/declarative/GridDeclarativeAttributeTest.java84
-rw-r--r--compatibility-server/src/test/java/com/vaadin/tests/server/component/grid/declarative/GridDeclarativeTestBase.java159
-rw-r--r--compatibility-server/src/test/java/com/vaadin/tests/server/component/grid/declarative/GridHeaderFooterDeclarativeTest.java356
-rw-r--r--compatibility-server/src/test/java/com/vaadin/tests/server/component/grid/declarative/GridInlineDataDeclarativeTest.java124
-rw-r--r--compatibility-server/src/test/java/com/vaadin/tests/server/component/grid/declarative/GridStructureDeclarativeTest.java50
-rw-r--r--compatibility-server/src/test/java/com/vaadin/tests/server/component/grid/sort/SortTest.java205
-rw-r--r--compatibility-server/src/test/java/com/vaadin/tests/server/renderer/ImageRendererTest.java85
-rw-r--r--compatibility-server/src/test/java/com/vaadin/tests/server/renderer/RendererTest.java268
-rw-r--r--compatibility-server/src/test/java/com/vaadin/tests/server/validation/BeanValidationTest.java129
-rw-r--r--compatibility-server/src/test/java/com/vaadin/ui/TableTest.java78
83 files changed, 23236 insertions, 1 deletions
diff --git a/compatibility-server/pom.xml b/compatibility-server/pom.xml
index 713c39c055..38dcc07f21 100644
--- a/compatibility-server/pom.xml
+++ b/compatibility-server/pom.xml
@@ -23,10 +23,27 @@
</dependency>
<dependency>
<groupId>${project.groupId}</groupId>
+ <artifactId>vaadin-server</artifactId>
+ <version>${project.version}</version>
+ <classifier>tests</classifier>
+ <scope>test</scope>
+ </dependency>
+ <dependency>
+ <groupId>${project.groupId}</groupId>
<artifactId>vaadin-compatibility-shared</artifactId>
<version>${project.version}</version>
</dependency>
-
+ <dependency>
+ <groupId>javax.servlet</groupId>
+ <artifactId>javax.servlet-api</artifactId>
+ <scope>provided</scope>
+ </dependency>
+ <!-- Bean Validation API -->
+ <dependency>
+ <groupId>javax.validation</groupId>
+ <artifactId>validation-api</artifactId>
+ <scope>provided</scope>
+ </dependency>
</dependencies>
<build>
diff --git a/compatibility-server/src/main/java/com/vaadin/data/fieldgroup/BeanFieldGroup.java b/compatibility-server/src/main/java/com/vaadin/data/fieldgroup/BeanFieldGroup.java
new file mode 100644
index 0000000000..96e4621761
--- /dev/null
+++ b/compatibility-server/src/main/java/com/vaadin/data/fieldgroup/BeanFieldGroup.java
@@ -0,0 +1,272 @@
+/*
+ * Copyright 2000-2016 Vaadin Ltd.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not
+ * use this file except in compliance with the License. You may obtain a copy of
+ * the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+package com.vaadin.data.fieldgroup;
+
+import java.beans.IntrospectionException;
+import java.lang.reflect.Method;
+import java.util.HashMap;
+import java.util.Map;
+
+import com.vaadin.data.Item;
+import com.vaadin.data.util.BeanItem;
+import com.vaadin.data.util.BeanUtil;
+import com.vaadin.v7.data.validator.LegacyBeanValidator;
+import com.vaadin.v7.ui.LegacyField;
+
+public class BeanFieldGroup<T> extends FieldGroup {
+
+ private final Class<T> beanType;
+
+ private static Boolean beanValidationImplementationAvailable = null;
+ private final Map<LegacyField<?>, LegacyBeanValidator> defaultValidators;
+
+ public BeanFieldGroup(Class<T> beanType) {
+ this.beanType = beanType;
+ this.defaultValidators = new HashMap<LegacyField<?>, LegacyBeanValidator>();
+ }
+
+ @Override
+ protected Class<?> getPropertyType(Object propertyId) {
+ if (getItemDataSource() != null) {
+ return super.getPropertyType(propertyId);
+ } else {
+ // Data source not set so we need to figure out the type manually
+ /*
+ * toString should never really be needed as propertyId should be of
+ * form "fieldName" or "fieldName.subField[.subField2]" but the
+ * method declaration comes from parent.
+ */
+ try {
+ Class<?> type = BeanUtil.getPropertyType(beanType,
+ propertyId.toString());
+ if (type == null) {
+ throw new BindException(
+ "Cannot determine type of propertyId '" + propertyId
+ + "'. The propertyId was not found in "
+ + beanType.getName());
+ }
+ return type;
+ } catch (IntrospectionException e) {
+ throw new BindException("Cannot determine type of propertyId '"
+ + propertyId + "'. Unable to introspect " + beanType,
+ e);
+ }
+ }
+ }
+
+ @Override
+ protected Object findPropertyId(java.lang.reflect.Field memberField) {
+ String fieldName = memberField.getName();
+ Item dataSource = getItemDataSource();
+ if (dataSource != null
+ && dataSource.getItemProperty(fieldName) != null) {
+ return fieldName;
+ } else {
+ String minifiedFieldName = minifyFieldName(fieldName);
+ try {
+ return getFieldName(beanType, minifiedFieldName);
+ } catch (SecurityException e) {
+ } catch (NoSuchFieldException e) {
+ }
+ }
+ return null;
+ }
+
+ private static String getFieldName(Class<?> cls, String propertyId)
+ throws SecurityException, NoSuchFieldException {
+ for (java.lang.reflect.Field field1 : cls.getDeclaredFields()) {
+ if (propertyId.equals(minifyFieldName(field1.getName()))) {
+ return field1.getName();
+ }
+ }
+ // Try super classes until we reach Object
+ Class<?> superClass = cls.getSuperclass();
+ if (superClass != null && superClass != Object.class) {
+ return getFieldName(superClass, propertyId);
+ } else {
+ throw new NoSuchFieldException();
+ }
+ }
+
+ /**
+ * Helper method for setting the data source directly using a bean. This
+ * method wraps the bean in a {@link BeanItem} and calls
+ * {@link #setItemDataSource(Item)}.
+ * <p>
+ * For null values, a null item is passed to
+ * {@link #setItemDataSource(Item)} to be properly clear fields.
+ *
+ * @param bean
+ * The bean to use as data source.
+ */
+ public void setItemDataSource(T bean) {
+ if (bean == null) {
+ setItemDataSource((Item) null);
+ } else {
+ setItemDataSource(new BeanItem<T>(bean, beanType));
+ }
+ }
+
+ @Override
+ public void setItemDataSource(Item item) {
+ if (item == null || (item instanceof BeanItem)) {
+ super.setItemDataSource(item);
+ } else {
+ throw new RuntimeException(getClass().getSimpleName()
+ + " only supports BeanItems as item data source");
+ }
+ }
+
+ @Override
+ public BeanItem<T> getItemDataSource() {
+ return (BeanItem<T>) super.getItemDataSource();
+ }
+
+ private void ensureNestedPropertyAdded(Object propertyId) {
+ if (getItemDataSource() != null) {
+ // The data source is set so the property must be found in the item.
+ // If it is not we try to add it.
+ try {
+ getItemProperty(propertyId);
+ } catch (BindException e) {
+ // Not found, try to add a nested property;
+ // BeanItem property ids are always strings so this is safe
+ getItemDataSource().addNestedProperty((String) propertyId);
+ }
+ }
+ }
+
+ @Override
+ public void bind(LegacyField field, Object propertyId) {
+ ensureNestedPropertyAdded(propertyId);
+ super.bind(field, propertyId);
+ }
+
+ @Override
+ public <T extends LegacyField> T buildAndBind(String caption,
+ Object propertyId, Class<T> fieldType) throws BindException {
+ ensureNestedPropertyAdded(propertyId);
+ return super.buildAndBind(caption, propertyId, fieldType);
+ }
+
+ @Override
+ public void unbind(LegacyField<?> field) throws BindException {
+ super.unbind(field);
+
+ LegacyBeanValidator removed = defaultValidators.remove(field);
+ if (removed != null) {
+ field.removeValidator(removed);
+ }
+ }
+
+ @Override
+ protected void configureField(LegacyField<?> field) {
+ super.configureField(field);
+ // Add Bean validators if there are annotations
+ if (isBeanValidationImplementationAvailable()
+ && !defaultValidators.containsKey(field)) {
+ LegacyBeanValidator validator = new LegacyBeanValidator(beanType,
+ getPropertyId(field).toString());
+ field.addValidator(validator);
+ if (field.getLocale() != null) {
+ validator.setLocale(field.getLocale());
+ }
+ defaultValidators.put(field, validator);
+ }
+ }
+
+ /**
+ * Checks whether a bean validation implementation (e.g. Hibernate Validator
+ * or Apache Bean Validation) is available.
+ *
+ * TODO move this method to some more generic location
+ *
+ * @return true if a JSR-303 bean validation implementation is available
+ */
+ protected static boolean isBeanValidationImplementationAvailable() {
+ if (beanValidationImplementationAvailable != null) {
+ return beanValidationImplementationAvailable;
+ }
+ try {
+ Class<?> validationClass = Class
+ .forName("javax.validation.Validation");
+ Method buildFactoryMethod = validationClass
+ .getMethod("buildDefaultValidatorFactory");
+ Object factory = buildFactoryMethod.invoke(null);
+ beanValidationImplementationAvailable = (factory != null);
+ } catch (Exception e) {
+ // no bean validation implementation available
+ beanValidationImplementationAvailable = false;
+ }
+ return beanValidationImplementationAvailable;
+ }
+
+ /**
+ * Convenience method to bind Fields from a given "field container" to a
+ * given bean with buffering disabled.
+ * <p>
+ * The returned {@link BeanFieldGroup} can be used for further
+ * configuration.
+ *
+ * @see #bindFieldsBuffered(Object, Object)
+ * @see #bindMemberFields(Object)
+ * @since 7.2
+ * @param bean
+ * the bean to be bound
+ * @param objectWithMemberFields
+ * the class that contains {@link LegacyField}s for bean
+ * properties
+ * @return the bean field group used to make binding
+ */
+ public static <T> BeanFieldGroup<T> bindFieldsUnbuffered(T bean,
+ Object objectWithMemberFields) {
+ return createAndBindFields(bean, objectWithMemberFields, false);
+ }
+
+ /**
+ * Convenience method to bind Fields from a given "field container" to a
+ * given bean with buffering enabled.
+ * <p>
+ * The returned {@link BeanFieldGroup} can be used for further
+ * configuration.
+ *
+ * @see #bindFieldsUnbuffered(Object, Object)
+ * @see #bindMemberFields(Object)
+ * @since 7.2
+ * @param bean
+ * the bean to be bound
+ * @param objectWithMemberFields
+ * the class that contains {@link LegacyField}s for bean
+ * properties
+ * @return the bean field group used to make binding
+ */
+ public static <T> BeanFieldGroup<T> bindFieldsBuffered(T bean,
+ Object objectWithMemberFields) {
+ return createAndBindFields(bean, objectWithMemberFields, true);
+ }
+
+ private static <T> BeanFieldGroup<T> createAndBindFields(T bean,
+ Object objectWithMemberFields, boolean buffered) {
+ @SuppressWarnings("unchecked")
+ BeanFieldGroup<T> beanFieldGroup = new BeanFieldGroup<T>(
+ (Class<T>) bean.getClass());
+ beanFieldGroup.setItemDataSource(bean);
+ beanFieldGroup.setBuffered(buffered);
+ beanFieldGroup.bindMemberFields(objectWithMemberFields);
+ return beanFieldGroup;
+ }
+
+}
diff --git a/compatibility-server/src/main/java/com/vaadin/data/fieldgroup/Caption.java b/compatibility-server/src/main/java/com/vaadin/data/fieldgroup/Caption.java
new file mode 100644
index 0000000000..d752aa78d2
--- /dev/null
+++ b/compatibility-server/src/main/java/com/vaadin/data/fieldgroup/Caption.java
@@ -0,0 +1,27 @@
+/*
+ * Copyright 2000-2016 Vaadin Ltd.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not
+ * use this file except in compliance with the License. You may obtain a copy of
+ * the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+package com.vaadin.data.fieldgroup;
+
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+@Target({ ElementType.FIELD })
+@Retention(RetentionPolicy.RUNTIME)
+public @interface Caption {
+ String value();
+}
diff --git a/compatibility-server/src/main/java/com/vaadin/data/fieldgroup/DefaultFieldGroupFieldFactory.java b/compatibility-server/src/main/java/com/vaadin/data/fieldgroup/DefaultFieldGroupFieldFactory.java
new file mode 100644
index 0000000000..24c97eedc5
--- /dev/null
+++ b/compatibility-server/src/main/java/com/vaadin/data/fieldgroup/DefaultFieldGroupFieldFactory.java
@@ -0,0 +1,254 @@
+/*
+ * Copyright 2000-2016 Vaadin Ltd.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not
+ * use this file except in compliance with the License. You may obtain a copy of
+ * the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+package com.vaadin.data.fieldgroup;
+
+import java.util.Date;
+import java.util.EnumSet;
+
+import com.vaadin.data.Item;
+import com.vaadin.data.fieldgroup.FieldGroup.BindException;
+import com.vaadin.ui.AbstractSelect;
+import com.vaadin.ui.ComboBox;
+import com.vaadin.ui.ListSelect;
+import com.vaadin.ui.NativeSelect;
+import com.vaadin.ui.OptionGroup;
+import com.vaadin.ui.RichTextArea;
+import com.vaadin.ui.Table;
+import com.vaadin.v7.ui.LegacyAbstractField;
+import com.vaadin.v7.ui.LegacyAbstractTextField;
+import com.vaadin.v7.ui.LegacyCheckBox;
+import com.vaadin.v7.ui.LegacyDateField;
+import com.vaadin.v7.ui.LegacyField;
+import com.vaadin.v7.ui.LegacyInlineDateField;
+import com.vaadin.v7.ui.LegacyPopupDateField;
+import com.vaadin.v7.ui.LegacyTextField;
+
+/**
+ * This class contains a basic implementation for {@link FieldGroupFieldFactory}
+ * .The class is singleton, use {@link #get()} method to get reference to the
+ * instance.
+ *
+ * @author Vaadin Ltd
+ */
+public class DefaultFieldGroupFieldFactory implements FieldGroupFieldFactory {
+
+ private static final DefaultFieldGroupFieldFactory INSTANCE = new DefaultFieldGroupFieldFactory();
+
+ public static final Object CAPTION_PROPERTY_ID = "Caption";
+
+ protected DefaultFieldGroupFieldFactory() {
+ }
+
+ /**
+ * Gets the singleton instance.
+ *
+ * @since 7.4
+ *
+ * @return the singleton instance
+ */
+ public static DefaultFieldGroupFieldFactory get() {
+ return INSTANCE;
+ }
+
+ @Override
+ public <T extends LegacyField> T createField(Class<?> type,
+ Class<T> fieldType) {
+ if (Enum.class.isAssignableFrom(type)) {
+ return createEnumField(type, fieldType);
+ } else if (Date.class.isAssignableFrom(type)) {
+ return createDateField(type, fieldType);
+ } else if (Boolean.class.isAssignableFrom(type)
+ || boolean.class.isAssignableFrom(type)) {
+ return createBooleanField(fieldType);
+ }
+ if (LegacyAbstractTextField.class.isAssignableFrom(fieldType)) {
+ return fieldType.cast(createAbstractTextField(
+ fieldType.asSubclass(LegacyAbstractTextField.class)));
+ } else if (fieldType == RichTextArea.class) {
+ return fieldType.cast(createRichTextArea());
+ }
+ return createDefaultField(type, fieldType);
+ }
+
+ protected RichTextArea createRichTextArea() {
+ RichTextArea rta = new RichTextArea();
+ rta.setImmediate(true);
+
+ return rta;
+ }
+
+ private <T extends LegacyField> T createEnumField(Class<?> type,
+ Class<T> fieldType) {
+ // Determine first if we should (or can) create a select for the enum
+ Class<AbstractSelect> selectClass = null;
+ if (AbstractSelect.class.isAssignableFrom(fieldType)) {
+ selectClass = (Class<AbstractSelect>) fieldType;
+ } else if (anySelect(fieldType)) {
+ selectClass = AbstractSelect.class;
+ }
+
+ if (selectClass != null) {
+ AbstractSelect s = createCompatibleSelect(selectClass);
+ populateWithEnumData(s, (Class<? extends Enum>) type);
+ return (T) s;
+ } else if (LegacyAbstractTextField.class.isAssignableFrom(fieldType)) {
+ return (T) createAbstractTextField(
+ (Class<? extends LegacyAbstractTextField>) fieldType);
+ }
+
+ return null;
+ }
+
+ private <T extends LegacyField> T createDateField(Class<?> type,
+ Class<T> fieldType) {
+ LegacyAbstractField field;
+
+ if (LegacyInlineDateField.class.isAssignableFrom(fieldType)) {
+ field = new LegacyInlineDateField();
+ } else if (anyField(fieldType)
+ || LegacyDateField.class.isAssignableFrom(fieldType)) {
+ field = new LegacyPopupDateField();
+ } else if (LegacyAbstractTextField.class.isAssignableFrom(fieldType)) {
+ field = createAbstractTextField(
+ (Class<? extends LegacyAbstractTextField>) fieldType);
+ } else {
+ return null;
+ }
+
+ field.setImmediate(true);
+ return (T) field;
+ }
+
+ protected AbstractSelect createCompatibleSelect(
+ Class<? extends AbstractSelect> fieldType) {
+ AbstractSelect select;
+ if (fieldType.isAssignableFrom(ListSelect.class)) {
+ select = new ListSelect();
+ select.setMultiSelect(false);
+ } else if (fieldType.isAssignableFrom(NativeSelect.class)) {
+ select = new NativeSelect();
+ } else if (fieldType.isAssignableFrom(OptionGroup.class)) {
+ select = new OptionGroup();
+ select.setMultiSelect(false);
+ } else if (fieldType.isAssignableFrom(Table.class)) {
+ Table t = new Table();
+ t.setSelectable(true);
+ select = t;
+ } else {
+ select = new ComboBox(null);
+ }
+ select.setImmediate(true);
+ select.setNullSelectionAllowed(false);
+
+ return select;
+ }
+
+ /**
+ * @since 7.4
+ * @param fieldType
+ * the type of the field
+ * @return true if any LegacyAbstractField can be assigned to the field
+ */
+ protected boolean anyField(Class<?> fieldType) {
+ return fieldType == LegacyField.class
+ || fieldType == LegacyAbstractField.class;
+ }
+
+ /**
+ * @since 7.4
+ * @param fieldType
+ * the type of the field
+ * @return true if any AbstractSelect can be assigned to the field
+ */
+ protected boolean anySelect(Class<? extends LegacyField> fieldType) {
+ return anyField(fieldType) || fieldType == AbstractSelect.class;
+ }
+
+ protected <T extends LegacyField> T createBooleanField(Class<T> fieldType) {
+ if (fieldType.isAssignableFrom(LegacyCheckBox.class)) {
+ LegacyCheckBox cb = new LegacyCheckBox(null);
+ cb.setImmediate(true);
+ return (T) cb;
+ } else if (LegacyAbstractTextField.class.isAssignableFrom(fieldType)) {
+ return (T) createAbstractTextField(
+ (Class<? extends LegacyAbstractTextField>) fieldType);
+ }
+
+ return null;
+ }
+
+ protected <T extends LegacyAbstractTextField> T createAbstractTextField(
+ Class<T> fieldType) {
+ if (fieldType == LegacyAbstractTextField.class) {
+ fieldType = (Class<T>) LegacyTextField.class;
+ }
+ try {
+ T field = fieldType.newInstance();
+ field.setImmediate(true);
+ return field;
+ } catch (Exception e) {
+ throw new BindException(
+ "Could not create a field of type " + fieldType, e);
+ }
+ }
+
+ /**
+ * Fallback when no specific field has been created. Typically returns a
+ * TextField.
+ *
+ * @param <T>
+ * The type of field to create
+ * @param type
+ * The type of data that should be edited
+ * @param fieldType
+ * The type of field to create
+ * @return A field capable of editing the data or null if no field could be
+ * created
+ */
+ protected <T extends LegacyField> T createDefaultField(Class<?> type,
+ Class<T> fieldType) {
+ if (fieldType.isAssignableFrom(LegacyTextField.class)) {
+ return fieldType
+ .cast(createAbstractTextField(LegacyTextField.class));
+ }
+ return null;
+ }
+
+ /**
+ * Populates the given select with all the enums in the given {@link Enum}
+ * class. Uses {@link Enum}.toString() for caption.
+ *
+ * @param select
+ * The select to populate
+ * @param enumClass
+ * The Enum class to use
+ */
+ protected void populateWithEnumData(AbstractSelect select,
+ Class<? extends Enum> enumClass) {
+ select.removeAllItems();
+ for (Object p : select.getContainerPropertyIds()) {
+ select.removeContainerProperty(p);
+ }
+ select.addContainerProperty(CAPTION_PROPERTY_ID, String.class, "");
+ select.setItemCaptionPropertyId(CAPTION_PROPERTY_ID);
+ @SuppressWarnings("unchecked")
+ EnumSet<?> enumSet = EnumSet.allOf(enumClass);
+ for (Object r : enumSet) {
+ Item newItem = select.addItem(r);
+ newItem.getItemProperty(CAPTION_PROPERTY_ID).setValue(r.toString());
+ }
+ }
+}
diff --git a/compatibility-server/src/main/java/com/vaadin/data/fieldgroup/FieldGroup.java b/compatibility-server/src/main/java/com/vaadin/data/fieldgroup/FieldGroup.java
new file mode 100644
index 0000000000..1254009cfc
--- /dev/null
+++ b/compatibility-server/src/main/java/com/vaadin/data/fieldgroup/FieldGroup.java
@@ -0,0 +1,1258 @@
+/*
+ * Copyright 2000-2016 Vaadin Ltd.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not
+ * use this file except in compliance with the License. You may obtain a copy of
+ * the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+package com.vaadin.data.fieldgroup;
+
+import java.io.Serializable;
+import java.lang.reflect.InvocationTargetException;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+
+import com.vaadin.data.Item;
+import com.vaadin.data.Property;
+import com.vaadin.data.util.TransactionalPropertyWrapper;
+import com.vaadin.ui.DefaultFieldFactory;
+import com.vaadin.util.ReflectTools;
+import com.vaadin.v7.data.Validator.InvalidValueException;
+import com.vaadin.v7.ui.LegacyAbstractField;
+import com.vaadin.v7.ui.LegacyField;
+
+/**
+ * FieldGroup provides an easy way of binding fields to data and handling
+ * commits of these fields.
+ * <p>
+ * The typical use case is to create a layout outside the FieldGroup and then
+ * use FieldGroup to bind the fields to a data source.
+ * </p>
+ * <p>
+ * {@link FieldGroup} is not a UI component so it cannot be added to a layout.
+ * Using the buildAndBind methods {@link FieldGroup} can create fields for you
+ * using a FieldGroupFieldFactory but you still have to add them to the correct
+ * position in your layout.
+ * </p>
+ *
+ * @author Vaadin Ltd
+ * @since 7.0
+ */
+public class FieldGroup implements Serializable {
+
+ private Item itemDataSource;
+ private boolean buffered = true;
+
+ private boolean enabled = true;
+ private boolean readOnly = false;
+
+ private HashMap<Object, LegacyField<?>> propertyIdToField = new HashMap<Object, LegacyField<?>>();
+ private LinkedHashMap<LegacyField<?>, Object> fieldToPropertyId = new LinkedHashMap<LegacyField<?>, Object>();
+ private List<CommitHandler> commitHandlers = new ArrayList<CommitHandler>();
+
+ /**
+ * The field factory used by builder methods.
+ */
+ private FieldGroupFieldFactory fieldFactory = DefaultFieldGroupFieldFactory
+ .get();
+
+ /**
+ * Constructs a field binder. Use {@link #setItemDataSource(Item)} to set a
+ * data source for the field binder.
+ *
+ */
+ public FieldGroup() {
+ }
+
+ /**
+ * Constructs a field binder that uses the given data source.
+ *
+ * @param itemDataSource
+ * The data source to bind the fields to
+ */
+ public FieldGroup(Item itemDataSource) {
+ setItemDataSource(itemDataSource);
+ }
+
+ /**
+ * Updates the item that is used by this FieldBinder. Rebinds all fields to
+ * the properties in the new item.
+ *
+ * @param itemDataSource
+ * The new item to use
+ */
+ public void setItemDataSource(Item itemDataSource) {
+ this.itemDataSource = itemDataSource;
+
+ for (LegacyField<?> f : fieldToPropertyId.keySet()) {
+ bind(f, fieldToPropertyId.get(f));
+ }
+ }
+
+ /**
+ * Gets the item used by this FieldBinder. Note that you must call
+ * {@link #commit()} for the item to be updated unless buffered mode has
+ * been switched off.
+ *
+ * @see #setBuffered(boolean)
+ * @see #commit()
+ *
+ * @return The item used by this FieldBinder
+ */
+ public Item getItemDataSource() {
+ return itemDataSource;
+ }
+
+ /**
+ * Checks the buffered mode for the bound fields.
+ * <p>
+ *
+ * @see #setBuffered(boolean) for more details on buffered mode
+ *
+ * @see LegacyField#isBuffered()
+ * @return true if buffered mode is on, false otherwise
+ *
+ */
+ public boolean isBuffered() {
+ return buffered;
+ }
+
+ /**
+ * Sets the buffered mode for the bound fields.
+ * <p>
+ * When buffered mode is on the item will not be updated until
+ * {@link #commit()} is called. If buffered mode is off the item will be
+ * updated once the fields are updated.
+ * </p>
+ * <p>
+ * The default is to use buffered mode.
+ * </p>
+ *
+ * @see LegacyField#setBuffered(boolean)
+ * @param buffered
+ * true to turn on buffered mode, false otherwise
+ */
+ public void setBuffered(boolean buffered) {
+ if (buffered == this.buffered) {
+ return;
+ }
+
+ this.buffered = buffered;
+ for (LegacyField<?> field : getFields()) {
+ field.setBuffered(buffered);
+ }
+ }
+
+ /**
+ * Returns the enabled status for the fields.
+ * <p>
+ * Note that this will not accurately represent the enabled status of all
+ * fields if you change the enabled status of the fields through some other
+ * method than {@link #setEnabled(boolean)}.
+ *
+ * @return true if the fields are enabled, false otherwise
+ */
+ public boolean isEnabled() {
+ return enabled;
+ }
+
+ /**
+ * Updates the enabled state of all bound fields.
+ *
+ * @param fieldsEnabled
+ * true to enable all bound fields, false to disable them
+ */
+ public void setEnabled(boolean fieldsEnabled) {
+ enabled = fieldsEnabled;
+ for (LegacyField<?> field : getFields()) {
+ field.setEnabled(fieldsEnabled);
+ }
+ }
+
+ /**
+ * Returns the read only status that is used by default with all fields that
+ * have a writable data source.
+ * <p>
+ * Note that this will not accurately represent the read only status of all
+ * fields if you change the read only status of the fields through some
+ * other method than {@link #setReadOnly(boolean)}.
+ *
+ * @return true if the fields are set to read only, false otherwise
+ */
+ public boolean isReadOnly() {
+ return readOnly;
+ }
+
+ /**
+ * Sets the read only state to the given value for all fields with writable
+ * data source. Fields with read only data source will always be set to read
+ * only.
+ *
+ * @param fieldsReadOnly
+ * true to set the fields with writable data source to read only,
+ * false to set them to read write
+ */
+ public void setReadOnly(boolean fieldsReadOnly) {
+ readOnly = fieldsReadOnly;
+ for (LegacyField<?> field : getFields()) {
+ if (field.getPropertyDataSource() == null
+ || !field.getPropertyDataSource().isReadOnly()) {
+ field.setReadOnly(fieldsReadOnly);
+ } else {
+ field.setReadOnly(true);
+ }
+ }
+ }
+
+ /**
+ * Returns a collection of all fields that have been bound.
+ * <p>
+ * The fields are not returned in any specific order.
+ * </p>
+ *
+ * @return A collection with all bound Fields
+ */
+ public Collection<LegacyField<?>> getFields() {
+ return fieldToPropertyId.keySet();
+ }
+
+ /**
+ * Binds the field with the given propertyId from the current item. If an
+ * item has not been set then the binding is postponed until the item is set
+ * using {@link #setItemDataSource(Item)}.
+ * <p>
+ * This method also adds validators when applicable.
+ * </p>
+ *
+ * @param field
+ * The field to bind
+ * @param propertyId
+ * The propertyId to bind to the field
+ * @throws BindException
+ * If the field is null or the property id is already bound to
+ * another field by this field binder
+ */
+ public void bind(LegacyField<?> field, Object propertyId)
+ throws BindException {
+ throwIfFieldIsNull(field, propertyId);
+ throwIfPropertyIdAlreadyBound(field, propertyId);
+
+ fieldToPropertyId.put(field, propertyId);
+ propertyIdToField.put(propertyId, field);
+ if (itemDataSource == null) {
+ // Clear any possible existing binding to clear the field
+ field.setPropertyDataSource(null);
+ boolean fieldReadOnly = field.isReadOnly();
+ if (!fieldReadOnly) {
+ field.clear();
+ } else {
+ // Temporarily make the field read-write so we can clear the
+ // value. Needed because setPropertyDataSource(null) does not
+ // currently clear the field
+ // (https://dev.vaadin.com/ticket/14733)
+ field.setReadOnly(false);
+ field.clear();
+ field.setReadOnly(true);
+ }
+
+ // Will be bound when data source is set
+ return;
+ }
+
+ field.setPropertyDataSource(
+ wrapInTransactionalProperty(getItemProperty(propertyId)));
+ configureField(field);
+ }
+
+ /**
+ * Wrap property to transactional property.
+ */
+ protected <T> Property.Transactional<T> wrapInTransactionalProperty(
+ Property<T> itemProperty) {
+ return new TransactionalPropertyWrapper<T>(itemProperty);
+ }
+
+ private void throwIfFieldIsNull(LegacyField<?> field, Object propertyId) {
+ if (field == null) {
+ throw new BindException(String.format(
+ "Cannot bind property id '%s' to a null field.",
+ propertyId));
+ }
+ }
+
+ private void throwIfPropertyIdAlreadyBound(LegacyField<?> field,
+ Object propertyId) {
+ if (propertyIdToField.containsKey(propertyId)
+ && propertyIdToField.get(propertyId) != field) {
+ throw new BindException("Property id " + propertyId
+ + " is already bound to another field");
+ }
+ }
+
+ /**
+ * Gets the property with the given property id from the item.
+ *
+ * @param propertyId
+ * The id if the property to find
+ * @return The property with the given id from the item
+ * @throws BindException
+ * If the property was not found in the item or no item has been
+ * set
+ */
+ protected Property getItemProperty(Object propertyId) throws BindException {
+ Item item = getItemDataSource();
+ if (item == null) {
+ throw new BindException("Could not lookup property with id "
+ + propertyId + " as no item has been set");
+ }
+ Property<?> p = item.getItemProperty(propertyId);
+ if (p == null) {
+ throw new BindException("A property with id " + propertyId
+ + " was not found in the item");
+ }
+ return p;
+ }
+
+ /**
+ * Detaches the field from its property id and removes it from this
+ * FieldBinder.
+ * <p>
+ * Note that the field is not detached from its property data source if it
+ * is no longer connected to the same property id it was bound to using this
+ * FieldBinder.
+ *
+ * @param field
+ * The field to detach
+ * @throws BindException
+ * If the field is not bound by this field binder or not bound
+ * to the correct property id
+ */
+ public void unbind(LegacyField<?> field) throws BindException {
+ Object propertyId = fieldToPropertyId.get(field);
+ if (propertyId == null) {
+ throw new BindException(
+ "The given field is not part of this FieldBinder");
+ }
+
+ TransactionalPropertyWrapper<?> wrapper = null;
+ Property fieldDataSource = field.getPropertyDataSource();
+ if (fieldDataSource instanceof TransactionalPropertyWrapper) {
+ wrapper = (TransactionalPropertyWrapper<?>) fieldDataSource;
+ fieldDataSource = ((TransactionalPropertyWrapper<?>) fieldDataSource)
+ .getWrappedProperty();
+
+ }
+ if (getItemDataSource() != null
+ && fieldDataSource == getItemProperty(propertyId)) {
+ if (null != wrapper) {
+ wrapper.detachFromProperty();
+ }
+ field.setPropertyDataSource(null);
+ }
+ fieldToPropertyId.remove(field);
+ propertyIdToField.remove(propertyId);
+ }
+
+ /**
+ * Configures a field with the settings set for this FieldBinder.
+ * <p>
+ * By default this updates the buffered, read only and enabled state of the
+ * field. Also adds validators when applicable. Fields with read only data
+ * source are always configured as read only.
+ *
+ * @param field
+ * The field to update
+ */
+ protected void configureField(LegacyField<?> field) {
+ field.setBuffered(isBuffered());
+
+ field.setEnabled(isEnabled());
+
+ if (field.getPropertyDataSource().isReadOnly()) {
+ field.setReadOnly(true);
+ } else {
+ field.setReadOnly(isReadOnly());
+ }
+ }
+
+ /**
+ * Gets the type of the property with the given property id.
+ *
+ * @param propertyId
+ * The propertyId. Must be find
+ * @return The type of the property
+ */
+ protected Class<?> getPropertyType(Object propertyId) throws BindException {
+ if (getItemDataSource() == null) {
+ throw new BindException("Property type for '" + propertyId
+ + "' could not be determined. No item data source has been set.");
+ }
+ Property<?> p = getItemDataSource().getItemProperty(propertyId);
+ if (p == null) {
+ throw new BindException("Property type for '" + propertyId
+ + "' could not be determined. No property with that id was found.");
+ }
+
+ return p.getType();
+ }
+
+ /**
+ * Returns a collection of all property ids that have been bound to fields.
+ * <p>
+ * Note that this will return property ids even before the item has been
+ * set. In that case it returns the property ids that will be bound once the
+ * item is set.
+ * </p>
+ * <p>
+ * No guarantee is given for the order of the property ids
+ * </p>
+ *
+ * @return A collection of bound property ids
+ */
+ public Collection<Object> getBoundPropertyIds() {
+ return Collections.unmodifiableCollection(propertyIdToField.keySet());
+ }
+
+ /**
+ * Returns a collection of all property ids that exist in the item set using
+ * {@link #setItemDataSource(Item)} but have not been bound to fields.
+ * <p>
+ * Will always return an empty collection before an item has been set using
+ * {@link #setItemDataSource(Item)}.
+ * </p>
+ * <p>
+ * No guarantee is given for the order of the property ids
+ * </p>
+ *
+ * @return A collection of property ids that have not been bound to fields
+ */
+ public Collection<Object> getUnboundPropertyIds() {
+ if (getItemDataSource() == null) {
+ return new ArrayList<Object>();
+ }
+ List<Object> unboundPropertyIds = new ArrayList<Object>();
+ unboundPropertyIds.addAll(getItemDataSource().getItemPropertyIds());
+ unboundPropertyIds.removeAll(propertyIdToField.keySet());
+ return unboundPropertyIds;
+ }
+
+ /**
+ * Commits all changes done to the bound fields.
+ * <p>
+ * Calls all {@link CommitHandler}s before and after committing the field
+ * changes to the item data source. The whole commit is aborted and state is
+ * restored to what it was before commit was called if any
+ * {@link CommitHandler} throws a CommitException or there is a problem
+ * committing the fields
+ *
+ * @throws CommitException
+ * If the commit was aborted
+ */
+ public void commit() throws CommitException {
+ if (!isBuffered()) {
+ // Not using buffered mode, nothing to do
+ return;
+ }
+
+ startTransactions();
+
+ try {
+ firePreCommitEvent();
+
+ Map<LegacyField<?>, InvalidValueException> invalidValueExceptions = commitFields();
+
+ if (invalidValueExceptions.isEmpty()) {
+ firePostCommitEvent();
+ commitTransactions();
+ } else {
+ throw new FieldGroupInvalidValueException(
+ invalidValueExceptions);
+ }
+ } catch (Exception e) {
+ rollbackTransactions();
+ throw new CommitException("Commit failed", this, e);
+ }
+
+ }
+
+ /**
+ * Tries to commit all bound fields one by one and gathers any validation
+ * exceptions in a map, which is returned to the caller
+ *
+ * @return a propertyId to validation exception map which is empty if all
+ * commits succeeded
+ */
+ private Map<LegacyField<?>, InvalidValueException> commitFields() {
+ Map<LegacyField<?>, InvalidValueException> invalidValueExceptions = new HashMap<LegacyField<?>, InvalidValueException>();
+
+ for (LegacyField<?> f : fieldToPropertyId.keySet()) {
+ try {
+ f.commit();
+ } catch (InvalidValueException e) {
+ invalidValueExceptions.put(f, e);
+ }
+ }
+
+ return invalidValueExceptions;
+ }
+
+ /**
+ * Exception which wraps InvalidValueExceptions from all invalid fields in a
+ * FieldGroup
+ *
+ * @since 7.4
+ */
+ public static class FieldGroupInvalidValueException
+ extends InvalidValueException {
+ private Map<LegacyField<?>, InvalidValueException> invalidValueExceptions;
+
+ /**
+ * Constructs a new exception with the specified validation exceptions.
+ *
+ * @param invalidValueExceptions
+ * a property id to exception map
+ */
+ public FieldGroupInvalidValueException(
+ Map<LegacyField<?>, InvalidValueException> invalidValueExceptions) {
+ super(null, invalidValueExceptions.values().toArray(
+ new InvalidValueException[invalidValueExceptions.size()]));
+ this.invalidValueExceptions = invalidValueExceptions;
+ }
+
+ /**
+ * Returns a map containing fields which failed validation and the
+ * exceptions the corresponding validators threw.
+ *
+ * @return a map with all the invalid value exceptions
+ */
+ public Map<LegacyField<?>, InvalidValueException> getInvalidFields() {
+ return invalidValueExceptions;
+ }
+ }
+
+ private void startTransactions() throws CommitException {
+ for (LegacyField<?> f : fieldToPropertyId.keySet()) {
+ Property.Transactional<?> property = (Property.Transactional<?>) f
+ .getPropertyDataSource();
+ if (property == null) {
+ throw new CommitException(
+ "Property \"" + fieldToPropertyId.get(f)
+ + "\" not bound to datasource.");
+ }
+ property.startTransaction();
+ }
+ }
+
+ private void commitTransactions() {
+ for (LegacyField<?> f : fieldToPropertyId.keySet()) {
+ ((Property.Transactional<?>) f.getPropertyDataSource()).commit();
+ }
+ }
+
+ private void rollbackTransactions() {
+ for (LegacyField<?> f : fieldToPropertyId.keySet()) {
+ try {
+ ((Property.Transactional<?>) f.getPropertyDataSource())
+ .rollback();
+ } catch (Exception rollbackException) {
+ // FIXME: What to do ?
+ }
+ }
+ }
+
+ /**
+ * Sends a preCommit event to all registered commit handlers
+ *
+ * @throws CommitException
+ * If the commit should be aborted
+ */
+ private void firePreCommitEvent() throws CommitException {
+ CommitHandler[] handlers = commitHandlers
+ .toArray(new CommitHandler[commitHandlers.size()]);
+
+ for (CommitHandler handler : handlers) {
+ handler.preCommit(new CommitEvent(this));
+ }
+ }
+
+ /**
+ * Sends a postCommit event to all registered commit handlers
+ *
+ * @throws CommitException
+ * If the commit should be aborted
+ */
+ private void firePostCommitEvent() throws CommitException {
+ CommitHandler[] handlers = commitHandlers
+ .toArray(new CommitHandler[commitHandlers.size()]);
+
+ for (CommitHandler handler : handlers) {
+ handler.postCommit(new CommitEvent(this));
+ }
+ }
+
+ /**
+ * Discards all changes done to the bound fields.
+ * <p>
+ * Only has effect if buffered mode is used.
+ *
+ */
+ public void discard() {
+ for (LegacyField<?> f : fieldToPropertyId.keySet()) {
+ try {
+ f.discard();
+ } catch (Exception e) {
+ // TODO: handle exception
+ // What can we do if discard fails other than try to discard all
+ // other fields?
+ }
+ }
+ }
+
+ /**
+ * Returns the field that is bound to the given property id
+ *
+ * @param propertyId
+ * The property id to use to lookup the field
+ * @return The field that is bound to the property id or null if no field is
+ * bound to that property id
+ */
+ public LegacyField<?> getField(Object propertyId) {
+ return propertyIdToField.get(propertyId);
+ }
+
+ /**
+ * Returns the property id that is bound to the given field
+ *
+ * @param field
+ * The field to use to lookup the property id
+ * @return The property id that is bound to the field or null if the field
+ * is not bound to any property id by this FieldBinder
+ */
+ public Object getPropertyId(LegacyField<?> field) {
+ return fieldToPropertyId.get(field);
+ }
+
+ /**
+ * Adds a commit handler.
+ * <p>
+ * The commit handler is called before the field values are committed to the
+ * item ( {@link CommitHandler#preCommit(CommitEvent)}) and after the item
+ * has been updated ({@link CommitHandler#postCommit(CommitEvent)}). If a
+ * {@link CommitHandler} throws a CommitException the whole commit is
+ * aborted and the fields retain their old values.
+ *
+ * @param commitHandler
+ * The commit handler to add
+ */
+ public void addCommitHandler(CommitHandler commitHandler) {
+ commitHandlers.add(commitHandler);
+ }
+
+ /**
+ * Removes the given commit handler.
+ *
+ * @see #addCommitHandler(CommitHandler)
+ *
+ * @param commitHandler
+ * The commit handler to remove
+ */
+ public void removeCommitHandler(CommitHandler commitHandler) {
+ commitHandlers.remove(commitHandler);
+ }
+
+ /**
+ * Returns a list of all commit handlers for this {@link FieldGroup}.
+ * <p>
+ * Use {@link #addCommitHandler(CommitHandler)} and
+ * {@link #removeCommitHandler(CommitHandler)} to register or unregister a
+ * commit handler.
+ *
+ * @return A collection of commit handlers
+ */
+ protected Collection<CommitHandler> getCommitHandlers() {
+ return Collections.unmodifiableCollection(commitHandlers);
+ }
+
+ /**
+ * CommitHandlers are used by {@link FieldGroup#commit()} as part of the
+ * commit transactions. CommitHandlers can perform custom operations as part
+ * of the commit and cause the commit to be aborted by throwing a
+ * {@link CommitException}.
+ */
+ public interface CommitHandler extends Serializable {
+ /**
+ * Called before changes are committed to the field and the item is
+ * updated.
+ * <p>
+ * Throw a {@link CommitException} to abort the commit.
+ *
+ * @param commitEvent
+ * An event containing information regarding the commit
+ * @throws CommitException
+ * if the commit should be aborted
+ */
+ public void preCommit(CommitEvent commitEvent) throws CommitException;
+
+ /**
+ * Called after changes are committed to the fields and the item is
+ * updated.
+ * <p>
+ * Throw a {@link CommitException} to abort the commit.
+ *
+ * @param commitEvent
+ * An event containing information regarding the commit
+ * @throws CommitException
+ * if the commit should be aborted
+ */
+ public void postCommit(CommitEvent commitEvent) throws CommitException;
+ }
+
+ /**
+ * FIXME javadoc
+ *
+ */
+ public static class CommitEvent implements Serializable {
+ private FieldGroup fieldBinder;
+
+ private CommitEvent(FieldGroup fieldBinder) {
+ this.fieldBinder = fieldBinder;
+ }
+
+ /**
+ * Returns the field binder that this commit relates to
+ *
+ * @return The FieldBinder that is being committed.
+ */
+ public FieldGroup getFieldBinder() {
+ return fieldBinder;
+ }
+
+ }
+
+ /**
+ * Checks the validity of the bound fields.
+ * <p>
+ * Call the {@link LegacyField#validate()} for the fields to get the
+ * individual error messages.
+ *
+ * @return true if all bound fields are valid, false otherwise.
+ */
+ public boolean isValid() {
+ try {
+ for (LegacyField<?> field : getFields()) {
+ field.validate();
+ }
+ return true;
+ } catch (InvalidValueException e) {
+ return false;
+ }
+ }
+
+ /**
+ * Checks if any bound field has been modified.
+ *
+ * @return true if at least one field has been modified, false otherwise
+ */
+ public boolean isModified() {
+ for (LegacyField<?> field : getFields()) {
+ if (field.isModified()) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ /**
+ * Gets the field factory for the {@link FieldGroup}. The field factory is
+ * only used when {@link FieldGroup} creates a new field.
+ *
+ * @return The field factory in use
+ *
+ */
+ public FieldGroupFieldFactory getFieldFactory() {
+ return fieldFactory;
+ }
+
+ /**
+ * Sets the field factory for the {@link FieldGroup}. The field factory is
+ * only used when {@link FieldGroup} creates a new field.
+ *
+ * @param fieldFactory
+ * The field factory to use
+ */
+ public void setFieldFactory(FieldGroupFieldFactory fieldFactory) {
+ this.fieldFactory = fieldFactory;
+ }
+
+ /**
+ * Binds member fields found in the given object.
+ * <p>
+ * This method processes all (Java) member fields whose type extends
+ * {@link LegacyField} and that can be mapped to a property id. Property id
+ * mapping is done based on the field name or on a @{@link PropertyId}
+ * annotation on the field. All non-null fields for which a property id can
+ * be determined are bound to the property id.
+ * </p>
+ * <p>
+ * For example:
+ *
+ * <pre>
+ * public class MyForm extends VerticalLayout {
+ * private TextField firstName = new TextField("First name");
+ * &#64;PropertyId("last")
+ * private TextField lastName = new TextField("Last name");
+ * private TextField age = new TextField("Age"); ... }
+ *
+ * MyForm myForm = new MyForm();
+ * ...
+ * fieldGroup.bindMemberFields(myForm);
+ * </pre>
+ *
+ * </p>
+ * This binds the firstName TextField to a "firstName" property in the item,
+ * lastName TextField to a "last" property and the age TextField to a "age"
+ * property.
+ *
+ * @param objectWithMemberFields
+ * The object that contains (Java) member fields to bind
+ * @throws BindException
+ * If there is a problem binding a field
+ */
+ public void bindMemberFields(Object objectWithMemberFields)
+ throws BindException {
+ buildAndBindMemberFields(objectWithMemberFields, false);
+ }
+
+ /**
+ * Binds member fields found in the given object and builds member fields
+ * that have not been initialized.
+ * <p>
+ * This method processes all (Java) member fields whose type extends
+ * {@link LegacyField} and that can be mapped to a property id. Property ids
+ * are searched in the following order: @{@link PropertyId} annotations,
+ * exact field name matches and the case-insensitive matching that ignores
+ * underscores. Fields that are not initialized (null) are built using the
+ * field factory. All non-null fields for which a property id can be
+ * determined are bound to the property id.
+ * </p>
+ * <p>
+ * For example:
+ *
+ * <pre>
+ * public class MyForm extends VerticalLayout {
+ * private TextField firstName = new TextField("First name");
+ * &#64;PropertyId("last")
+ * private TextField lastName = new TextField("Last name");
+ * private TextField age;
+ *
+ * MyForm myForm = new MyForm();
+ * ...
+ * fieldGroup.buildAndBindMemberFields(myForm);
+ * </pre>
+ *
+ * </p>
+ * <p>
+ * This binds the firstName TextField to a "firstName" property in the item,
+ * lastName TextField to a "last" property and builds an age TextField using
+ * the field factory and then binds it to the "age" property.
+ * </p>
+ *
+ * @param objectWithMemberFields
+ * The object that contains (Java) member fields to build and
+ * bind
+ * @throws BindException
+ * If there is a problem binding or building a field
+ */
+ public void buildAndBindMemberFields(Object objectWithMemberFields)
+ throws BindException {
+ buildAndBindMemberFields(objectWithMemberFields, true);
+ }
+
+ /**
+ * Binds member fields found in the given object and optionally builds
+ * member fields that have not been initialized.
+ * <p>
+ * This method processes all (Java) member fields whose type extends
+ * {@link LegacyField} and that can be mapped to a property id. Property ids
+ * are searched in the following order: @{@link PropertyId} annotations,
+ * exact field name matches and the case-insensitive matching that ignores
+ * underscores. Fields that are not initialized (null) are built using the
+ * field factory is buildFields is true. All non-null fields for which a
+ * property id can be determined are bound to the property id.
+ * </p>
+ *
+ * @param objectWithMemberFields
+ * The object that contains (Java) member fields to build and
+ * bind
+ * @throws BindException
+ * If there is a problem binding or building a field
+ */
+ protected void buildAndBindMemberFields(Object objectWithMemberFields,
+ boolean buildFields) throws BindException {
+ Class<?> objectClass = objectWithMemberFields.getClass();
+
+ for (java.lang.reflect.Field memberField : getFieldsInDeclareOrder(
+ objectClass)) {
+
+ if (!LegacyField.class.isAssignableFrom(memberField.getType())) {
+ // Process next field
+ continue;
+ }
+
+ PropertyId propertyIdAnnotation = memberField
+ .getAnnotation(PropertyId.class);
+
+ Class<? extends LegacyField> fieldType = (Class<? extends LegacyField>) memberField
+ .getType();
+
+ Object propertyId = null;
+ if (propertyIdAnnotation != null) {
+ // @PropertyId(propertyId) always overrides property id
+ propertyId = propertyIdAnnotation.value();
+ } else {
+ try {
+ propertyId = findPropertyId(memberField);
+ } catch (SearchException e) {
+ // Property id was not found, skip this field
+ continue;
+ }
+ if (propertyId == null) {
+ // Property id was not found, skip this field
+ continue;
+ }
+ }
+
+ // Ensure that the property id exists
+ Class<?> propertyType;
+
+ try {
+ propertyType = getPropertyType(propertyId);
+ } catch (BindException e) {
+ // Property id was not found, skip this field
+ continue;
+ }
+
+ LegacyField<?> field;
+ try {
+ // Get the field from the object
+ field = (LegacyField<?>) ReflectTools.getJavaFieldValue(
+ objectWithMemberFields, memberField, LegacyField.class);
+ } catch (Exception e) {
+ // If we cannot determine the value, just skip the field and try
+ // the next one
+ continue;
+ }
+
+ if (field == null && buildFields) {
+ Caption captionAnnotation = memberField
+ .getAnnotation(Caption.class);
+ String caption;
+ if (captionAnnotation != null) {
+ caption = captionAnnotation.value();
+ } else {
+ caption = DefaultFieldFactory
+ .createCaptionByPropertyId(propertyId);
+ }
+
+ // Create the component (LegacyField)
+ field = build(caption, propertyType, fieldType);
+
+ // Store it in the field
+ try {
+ ReflectTools.setJavaFieldValue(objectWithMemberFields,
+ memberField, field);
+ } catch (IllegalArgumentException e) {
+ throw new BindException("Could not assign value to field '"
+ + memberField.getName() + "'", e);
+ } catch (IllegalAccessException e) {
+ throw new BindException("Could not assign value to field '"
+ + memberField.getName() + "'", e);
+ } catch (InvocationTargetException e) {
+ throw new BindException("Could not assign value to field '"
+ + memberField.getName() + "'", e);
+ }
+ }
+
+ if (field != null) {
+ // Bind it to the property id
+ bind(field, propertyId);
+ }
+ }
+ }
+
+ /**
+ * Searches for a property id from the current itemDataSource that matches
+ * the given memberField.
+ * <p>
+ * If perfect match is not found, uses a case insensitive search that also
+ * ignores underscores. Returns null if no match is found. Throws a
+ * SearchException if no item data source has been set.
+ * </p>
+ * <p>
+ * The propertyId search logic used by
+ * {@link #buildAndBindMemberFields(Object, boolean)
+ * buildAndBindMemberFields} can easily be customized by overriding this
+ * method. No other changes are needed.
+ * </p>
+ *
+ * @param memberField
+ * The field an object id is searched for
+ * @return
+ */
+ protected Object findPropertyId(java.lang.reflect.Field memberField) {
+ String fieldName = memberField.getName();
+ if (getItemDataSource() == null) {
+ throw new SearchException("Property id type for field '" + fieldName
+ + "' could not be determined. No item data source has been set.");
+ }
+ Item dataSource = getItemDataSource();
+ if (dataSource.getItemProperty(fieldName) != null) {
+ return fieldName;
+ } else {
+ String minifiedFieldName = minifyFieldName(fieldName);
+ for (Object itemPropertyId : dataSource.getItemPropertyIds()) {
+ if (itemPropertyId instanceof String) {
+ String itemPropertyName = (String) itemPropertyId;
+ if (minifiedFieldName
+ .equals(minifyFieldName(itemPropertyName))) {
+ return itemPropertyName;
+ }
+ }
+ }
+ }
+ return null;
+ }
+
+ protected static String minifyFieldName(String fieldName) {
+ return fieldName.toLowerCase().replace("_", "");
+ }
+
+ /**
+ * Exception thrown by a FieldGroup when the commit operation fails.
+ *
+ * Provides information about validation errors through
+ * {@link #getInvalidFields()} if the cause of the failure is that all bound
+ * fields did not pass validation
+ *
+ */
+ public static class CommitException extends Exception {
+
+ private FieldGroup fieldGroup;
+
+ public CommitException() {
+ super();
+ }
+
+ public CommitException(String message, FieldGroup fieldGroup,
+ Throwable cause) {
+ super(message, cause);
+ this.fieldGroup = fieldGroup;
+ }
+
+ public CommitException(String message, Throwable cause) {
+ super(message, cause);
+ }
+
+ public CommitException(String message) {
+ super(message);
+ }
+
+ public CommitException(Throwable cause) {
+ super(cause);
+ }
+
+ /**
+ * Returns a map containing the fields which failed validation and the
+ * exceptions the corresponding validators threw.
+ *
+ * @since 7.4
+ * @return a map with all the invalid value exceptions. Can be empty but
+ * not null
+ */
+ public Map<LegacyField<?>, InvalidValueException> getInvalidFields() {
+ if (getCause() instanceof FieldGroupInvalidValueException) {
+ return ((FieldGroupInvalidValueException) getCause())
+ .getInvalidFields();
+ }
+ return new HashMap<LegacyField<?>, InvalidValueException>();
+ }
+
+ /**
+ * Returns the field group where the exception occurred
+ *
+ * @since 7.4
+ * @return the field group
+ */
+ public FieldGroup getFieldGroup() {
+ return fieldGroup;
+ }
+
+ }
+
+ public static class BindException extends RuntimeException {
+
+ public BindException(String message) {
+ super(message);
+ }
+
+ public BindException(String message, Throwable t) {
+ super(message, t);
+ }
+
+ }
+
+ public static class SearchException extends RuntimeException {
+
+ public SearchException(String message) {
+ super(message);
+ }
+
+ public SearchException(String message, Throwable t) {
+ super(message, t);
+ }
+
+ }
+
+ /**
+ * Builds a field and binds it to the given property id using the field
+ * binder.
+ *
+ * @param propertyId
+ * The property id to bind to. Must be present in the field
+ * finder.
+ * @throws BindException
+ * If there is a problem while building or binding
+ * @return The created and bound field
+ */
+ public LegacyField<?> buildAndBind(Object propertyId) throws BindException {
+ String caption = DefaultFieldFactory
+ .createCaptionByPropertyId(propertyId);
+ return buildAndBind(caption, propertyId);
+ }
+
+ /**
+ * Builds a field using the given caption and binds it to the given property
+ * id using the field binder.
+ *
+ * @param caption
+ * The caption for the field
+ * @param propertyId
+ * The property id to bind to. Must be present in the field
+ * finder.
+ * @throws BindException
+ * If there is a problem while building or binding
+ * @return The created and bound field. Can be any type of
+ * {@link LegacyField}.
+ */
+ public LegacyField<?> buildAndBind(String caption, Object propertyId)
+ throws BindException {
+ return buildAndBind(caption, propertyId, LegacyField.class);
+ }
+
+ /**
+ * Builds a field using the given caption and binds it to the given property
+ * id using the field binder. Ensures the new field is of the given type.
+ *
+ * @param caption
+ * The caption for the field
+ * @param propertyId
+ * The property id to bind to. Must be present in the field
+ * finder.
+ * @throws BindException
+ * If the field could not be created
+ * @return The created and bound field. Can be any type of
+ * {@link LegacyField}.
+ */
+
+ public <T extends LegacyField> T buildAndBind(String caption,
+ Object propertyId, Class<T> fieldType) throws BindException {
+ Class<?> type = getPropertyType(propertyId);
+
+ T field = build(caption, type, fieldType);
+ bind(field, propertyId);
+
+ return field;
+ }
+
+ /**
+ * Creates a field based on the given data type.
+ * <p>
+ * The data type is the type that we want to edit using the field. The field
+ * type is the type of field we want to create, can be {@link LegacyField}
+ * if any LegacyField is good.
+ * </p>
+ *
+ * @param caption
+ * The caption for the new field
+ * @param dataType
+ * The data model type that we want to edit using the field
+ * @param fieldType
+ * The type of field that we want to create
+ * @return A LegacyField capable of editing the given type
+ * @throws BindException
+ * If the field could not be created
+ */
+ protected <T extends LegacyField> T build(String caption, Class<?> dataType,
+ Class<T> fieldType) throws BindException {
+ T field = getFieldFactory().createField(dataType, fieldType);
+ if (field == null) {
+ throw new BindException(
+ "Unable to build a field of type " + fieldType.getName()
+ + " for editing " + dataType.getName());
+ }
+
+ field.setCaption(caption);
+ return field;
+ }
+
+ /**
+ * Returns an array containing LegacyField objects reflecting all the fields
+ * of the class or interface represented by this Class object. The elements
+ * in the array returned are sorted in declare order from sub class to super
+ * class.
+ *
+ * @param searchClass
+ * @return
+ */
+ protected static List<java.lang.reflect.Field> getFieldsInDeclareOrder(
+ Class searchClass) {
+ ArrayList<java.lang.reflect.Field> memberFieldInOrder = new ArrayList<java.lang.reflect.Field>();
+
+ while (searchClass != null) {
+ for (java.lang.reflect.Field memberField : searchClass
+ .getDeclaredFields()) {
+ memberFieldInOrder.add(memberField);
+ }
+ searchClass = searchClass.getSuperclass();
+ }
+ return memberFieldInOrder;
+ }
+
+ /**
+ * Clears the value of all fields.
+ *
+ * @since 7.4
+ */
+ public void clear() {
+ for (LegacyField<?> f : getFields()) {
+ if (f instanceof LegacyAbstractField) {
+ ((LegacyAbstractField) f).clear();
+ }
+ }
+
+ }
+
+} \ No newline at end of file
diff --git a/compatibility-server/src/main/java/com/vaadin/data/fieldgroup/FieldGroupFieldFactory.java b/compatibility-server/src/main/java/com/vaadin/data/fieldgroup/FieldGroupFieldFactory.java
new file mode 100644
index 0000000000..a80d51c6df
--- /dev/null
+++ b/compatibility-server/src/main/java/com/vaadin/data/fieldgroup/FieldGroupFieldFactory.java
@@ -0,0 +1,43 @@
+/*
+ * Copyright 2000-2016 Vaadin Ltd.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not
+ * use this file except in compliance with the License. You may obtain a copy of
+ * the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+package com.vaadin.data.fieldgroup;
+
+import java.io.Serializable;
+
+import com.vaadin.v7.ui.LegacyField;
+
+/**
+ * Factory interface for creating new LegacyField-instances based on the data
+ * type that should be edited.
+ *
+ * @author Vaadin Ltd.
+ * @since 7.0
+ */
+public interface FieldGroupFieldFactory extends Serializable {
+ /**
+ * Creates a field based on the data type that we want to edit
+ *
+ * @param dataType
+ * The type that we want to edit using the field
+ * @param fieldType
+ * The type of field we want to create. If set to
+ * {@link LegacyField} then any type of field is accepted
+ * @return A field that can be assigned to the given fieldType and that is
+ * capable of editing the given type of data
+ */
+ <T extends LegacyField> T createField(Class<?> dataType,
+ Class<T> fieldType);
+}
diff --git a/compatibility-server/src/main/java/com/vaadin/data/fieldgroup/PropertyId.java b/compatibility-server/src/main/java/com/vaadin/data/fieldgroup/PropertyId.java
new file mode 100644
index 0000000000..a2a8aa27af
--- /dev/null
+++ b/compatibility-server/src/main/java/com/vaadin/data/fieldgroup/PropertyId.java
@@ -0,0 +1,60 @@
+/*
+ * Copyright 2000-2016 Vaadin Ltd.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not
+ * use this file except in compliance with the License. You may obtain a copy of
+ * the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+package com.vaadin.data.fieldgroup;
+
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+/**
+ * Defines the custom property name to be bound to a {@link LegacyField} using
+ * {@link FieldGroup} or {@link BeanFieldGroup}.
+ * <p>
+ * The automatic data binding in FieldGroup and BeanFieldGroup relies on a
+ * naming convention by default: properties of an item are bound to similarly
+ * named field components in given a editor object. If you want to map a
+ * property with a different name (ID) to a
+ * {@link com.vaadin.client.ui.LegacyField}, you can use this annotation for the
+ * member fields, with the name (ID) of the desired property as the parameter.
+ * <p>
+ * In following usage example, the text field would be bound to property "foo"
+ * in the Entity class. <code>
+ * <pre>
+ * class Editor extends FormLayout {
+ &#64;PropertyId("foo")
+ TextField myField = new TextField();
+ }
+
+ class Entity {
+ String foo;
+ }
+
+ {
+ Editor e = new Editor();
+ BeanFieldGroup.bindFieldsUnbuffered(new Entity(), e);
+ }
+ </pre>
+ * </code>
+ *
+ * @since 7.0
+ * @author Vaadin Ltd
+ */
+@Target({ ElementType.FIELD })
+@Retention(RetentionPolicy.RUNTIME)
+public @interface PropertyId {
+ String value();
+}
diff --git a/compatibility-server/src/main/java/com/vaadin/server/communication/data/DataGenerator.java b/compatibility-server/src/main/java/com/vaadin/server/communication/data/DataGenerator.java
new file mode 100644
index 0000000000..f7459a7dd3
--- /dev/null
+++ b/compatibility-server/src/main/java/com/vaadin/server/communication/data/DataGenerator.java
@@ -0,0 +1,58 @@
+/*
+ * Copyright 2000-2016 Vaadin Ltd.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not
+ * use this file except in compliance with the License. You may obtain a copy of
+ * the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+package com.vaadin.server.communication.data;
+
+import java.io.Serializable;
+
+import com.vaadin.data.Item;
+import com.vaadin.ui.LegacyGrid.AbstractGridExtension;
+
+import elemental.json.JsonObject;
+
+/**
+ * Interface for {@link AbstractGridExtension}s that allows adding data to row
+ * objects being sent to client by the {@link RpcDataProviderExtension}.
+ * <p>
+ * This class also provides a way to remove any unneeded data once the data
+ * object is no longer used on the client-side.
+ *
+ * @since 7.6
+ * @author Vaadin Ltd
+ */
+public interface DataGenerator extends Serializable {
+
+ /**
+ * Adds data to row object for given item and item id being sent to client.
+ *
+ * @param itemId
+ * item id of item
+ * @param item
+ * item being sent to client
+ * @param rowData
+ * row object being sent to client
+ */
+ public void generateData(Object itemId, Item item, JsonObject rowData);
+
+ /**
+ * Informs the DataGenerator that an item id has been dropped and is no
+ * longer needed. This method should clean up any unneeded stored data
+ * related to the item.
+ *
+ * @param itemId
+ * removed item id
+ */
+ public void destroyData(Object itemId);
+}
diff --git a/compatibility-server/src/main/java/com/vaadin/server/communication/data/RpcDataProviderExtension.java b/compatibility-server/src/main/java/com/vaadin/server/communication/data/RpcDataProviderExtension.java
new file mode 100644
index 0000000000..341c3f1a45
--- /dev/null
+++ b/compatibility-server/src/main/java/com/vaadin/server/communication/data/RpcDataProviderExtension.java
@@ -0,0 +1,632 @@
+/*
+ * Copyright 2000-2016 Vaadin Ltd.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not
+ * use this file except in compliance with the License. You may obtain a copy of
+ * the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+
+package com.vaadin.server.communication.data;
+
+import java.io.Serializable;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.LinkedHashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+import com.vaadin.data.Container;
+import com.vaadin.data.Container.Indexed;
+import com.vaadin.data.Container.Indexed.ItemAddEvent;
+import com.vaadin.data.Container.Indexed.ItemRemoveEvent;
+import com.vaadin.data.Container.ItemSetChangeEvent;
+import com.vaadin.data.Container.ItemSetChangeListener;
+import com.vaadin.data.Container.ItemSetChangeNotifier;
+import com.vaadin.data.Item;
+import com.vaadin.data.Property;
+import com.vaadin.data.Property.ValueChangeEvent;
+import com.vaadin.data.Property.ValueChangeListener;
+import com.vaadin.data.Property.ValueChangeNotifier;
+import com.vaadin.server.AbstractExtension;
+import com.vaadin.server.ClientConnector;
+import com.vaadin.server.KeyMapper;
+import com.vaadin.shared.data.DataProviderRpc;
+import com.vaadin.shared.data.DataRequestRpc;
+import com.vaadin.shared.ui.grid.GridState;
+import com.vaadin.shared.ui.grid.Range;
+import com.vaadin.ui.LegacyGrid;
+import com.vaadin.ui.LegacyGrid.Column;
+
+import elemental.json.Json;
+import elemental.json.JsonArray;
+import elemental.json.JsonObject;
+
+/**
+ * Provides Vaadin server-side container data source to a
+ * {@link com.vaadin.client.ui.grid.GridConnector}. This is currently
+ * implemented as an Extension hardcoded to support a specific connector type.
+ * This will be changed once framework support for something more flexible has
+ * been implemented.
+ *
+ * @since 7.4
+ * @author Vaadin Ltd
+ */
+public class RpcDataProviderExtension extends AbstractExtension {
+
+ /**
+ * Class for keeping track of current items and ValueChangeListeners.
+ *
+ * @since 7.6
+ */
+ private class ActiveItemHandler implements Serializable, DataGenerator {
+
+ private final Map<Object, GridValueChangeListener> activeItemMap = new HashMap<Object, GridValueChangeListener>();
+ private final KeyMapper<Object> keyMapper = new KeyMapper<Object>();
+ private final Set<Object> droppedItems = new HashSet<Object>();
+
+ /**
+ * Registers ValueChangeListeners for given item ids.
+ * <p>
+ * Note: This method will clean up any unneeded listeners and key
+ * mappings
+ *
+ * @param itemIds
+ * collection of new active item ids
+ */
+ public void addActiveItems(Collection<?> itemIds) {
+ for (Object itemId : itemIds) {
+ if (!activeItemMap.containsKey(itemId)) {
+ activeItemMap.put(itemId, new GridValueChangeListener(
+ itemId, container.getItem(itemId)));
+ }
+ }
+
+ // Remove still active rows that were "dropped"
+ droppedItems.removeAll(itemIds);
+ internalDropItems(droppedItems);
+ droppedItems.clear();
+ }
+
+ /**
+ * Marks given item id as dropped. Dropped items are cleared when adding
+ * new active items.
+ *
+ * @param itemId
+ * dropped item id
+ */
+ public void dropActiveItem(Object itemId) {
+ if (activeItemMap.containsKey(itemId)) {
+ droppedItems.add(itemId);
+ }
+ }
+
+ /**
+ * Gets a collection copy of currently active item ids.
+ *
+ * @return collection of item ids
+ */
+ public Collection<Object> getActiveItemIds() {
+ return new HashSet<Object>(activeItemMap.keySet());
+ }
+
+ /**
+ * Gets a collection copy of currently active ValueChangeListeners.
+ *
+ * @return collection of value change listeners
+ */
+ public Collection<GridValueChangeListener> getValueChangeListeners() {
+ return new HashSet<GridValueChangeListener>(activeItemMap.values());
+ }
+
+ @Override
+ public void generateData(Object itemId, Item item, JsonObject rowData) {
+ rowData.put(GridState.JSONKEY_ROWKEY, keyMapper.key(itemId));
+ }
+
+ @Override
+ public void destroyData(Object itemId) {
+ keyMapper.remove(itemId);
+ removeListener(itemId);
+ }
+
+ private void removeListener(Object itemId) {
+ GridValueChangeListener removed = activeItemMap.remove(itemId);
+
+ if (removed != null) {
+ removed.removeListener();
+ }
+ }
+ }
+
+ /**
+ * A class to listen to changes in property values in the Container added
+ * with {@link LegacyGrid#setContainerDatasource(Container.Indexed)}, and notifies
+ * the data source to update the client-side representation of the modified
+ * item.
+ * <p>
+ * One instance of this class can (and should) be reused for all the
+ * properties in an item, since this class will inform that the entire row
+ * needs to be re-evaluated (in contrast to a property-based change
+ * management)
+ * <p>
+ * Since there's no Container-wide possibility to listen to any kind of
+ * value changes, an instance of this class needs to be attached to each and
+ * every Item's Property in the container.
+ *
+ * @see LegacyGrid#addValueChangeListener(Container, Object, Object)
+ * @see LegacyGrid#valueChangeListeners
+ */
+ private class GridValueChangeListener implements ValueChangeListener {
+ private final Object itemId;
+ private final Item item;
+
+ public GridValueChangeListener(Object itemId, Item item) {
+ /*
+ * Using an assert instead of an exception throw, just to optimize
+ * prematurely
+ */
+ assert itemId != null : "null itemId not accepted";
+ this.itemId = itemId;
+ this.item = item;
+
+ internalAddColumns(getGrid().getColumns());
+ }
+
+ @Override
+ public void valueChange(ValueChangeEvent event) {
+ updateRowData(itemId);
+ }
+
+ public void removeListener() {
+ removeColumns(getGrid().getColumns());
+ }
+
+ public void addColumns(Collection<Column> addedColumns) {
+ internalAddColumns(addedColumns);
+ updateRowData(itemId);
+ }
+
+ private void internalAddColumns(Collection<Column> addedColumns) {
+ for (final Column column : addedColumns) {
+ final Property<?> property = item
+ .getItemProperty(column.getPropertyId());
+ if (property instanceof ValueChangeNotifier) {
+ ((ValueChangeNotifier) property)
+ .addValueChangeListener(this);
+ }
+ }
+ }
+
+ public void removeColumns(Collection<Column> removedColumns) {
+ for (final Column column : removedColumns) {
+ final Property<?> property = item
+ .getItemProperty(column.getPropertyId());
+ if (property instanceof ValueChangeNotifier) {
+ ((ValueChangeNotifier) property)
+ .removeValueChangeListener(this);
+ }
+ }
+ }
+ }
+
+ private final Indexed container;
+
+ private DataProviderRpc rpc;
+
+ private final ItemSetChangeListener itemListener = new ItemSetChangeListener() {
+ @Override
+ public void containerItemSetChange(ItemSetChangeEvent event) {
+
+ if (event instanceof ItemAddEvent) {
+ ItemAddEvent addEvent = (ItemAddEvent) event;
+ int firstIndex = addEvent.getFirstIndex();
+ int count = addEvent.getAddedItemsCount();
+ insertRowData(firstIndex, count);
+ }
+
+ else if (event instanceof ItemRemoveEvent) {
+ ItemRemoveEvent removeEvent = (ItemRemoveEvent) event;
+ int firstIndex = removeEvent.getFirstIndex();
+ int count = removeEvent.getRemovedItemsCount();
+ removeRowData(firstIndex, count);
+ }
+
+ else {
+ // Remove obsolete value change listeners.
+ Set<Object> keySet = new HashSet<Object>(
+ activeItemHandler.activeItemMap.keySet());
+ for (Object itemId : keySet) {
+ activeItemHandler.removeListener(itemId);
+ }
+
+ /* Mark as dirty to push changes in beforeClientResponse */
+ bareItemSetTriggeredSizeChange = true;
+ markAsDirty();
+ }
+ }
+ };
+
+ /** RpcDataProvider should send the current cache again. */
+ private boolean refreshCache = false;
+
+ /** Set of updated item ids */
+ private transient Set<Object> updatedItemIds;
+
+ /**
+ * Queued RPC calls for adding and removing rows. Queue will be handled in
+ * {@link beforeClientResponse}
+ */
+ private transient List<Runnable> rowChanges;
+
+ /** Size possibly changed with a bare ItemSetChangeEvent */
+ private boolean bareItemSetTriggeredSizeChange = false;
+
+ private final Set<DataGenerator> dataGenerators = new LinkedHashSet<DataGenerator>();
+
+ private final ActiveItemHandler activeItemHandler = new ActiveItemHandler();
+
+ /**
+ * Creates a new data provider using the given container.
+ *
+ * @param container
+ * the container to make available
+ */
+ public RpcDataProviderExtension(Indexed container) {
+ this.container = container;
+ rpc = getRpcProxy(DataProviderRpc.class);
+
+ registerRpc(new DataRequestRpc() {
+ @Override
+ public void requestRows(int firstRow, int numberOfRows,
+ int firstCachedRowIndex, int cacheSize) {
+ pushRowData(firstRow, numberOfRows, firstCachedRowIndex,
+ cacheSize);
+ }
+
+ @Override
+ public void dropRows(JsonArray rowKeys) {
+ for (int i = 0; i < rowKeys.length(); ++i) {
+ activeItemHandler.dropActiveItem(
+ getKeyMapper().get(rowKeys.getString(i)));
+ }
+ }
+ });
+
+ if (container instanceof ItemSetChangeNotifier) {
+ ((ItemSetChangeNotifier) container)
+ .addItemSetChangeListener(itemListener);
+ }
+
+ addDataGenerator(activeItemHandler);
+ }
+
+ /**
+ * {@inheritDoc}
+ * <p>
+ * RpcDataProviderExtension makes all actual RPC calls from this function
+ * based on changes in the container.
+ */
+ @Override
+ public void beforeClientResponse(boolean initial) {
+ if (initial || bareItemSetTriggeredSizeChange) {
+ /*
+ * Push initial set of rows, assuming Grid will initially be
+ * rendered scrolled to the top and with a decent amount of rows
+ * visible. If this guess is right, initial data can be shown
+ * without a round-trip and if it's wrong, the data will simply be
+ * discarded.
+ */
+ int size = container.size();
+ rpc.resetDataAndSize(size);
+
+ int numberOfRows = Math.min(40, size);
+ pushRowData(0, numberOfRows, 0, 0);
+ } else {
+ // Only do row changes if not initial response.
+ if (rowChanges != null) {
+ for (Runnable r : rowChanges) {
+ r.run();
+ }
+ }
+
+ // Send current rows again if needed.
+ if (refreshCache) {
+ for (Object itemId : activeItemHandler.getActiveItemIds()) {
+ updateRowData(itemId);
+ }
+ }
+ }
+
+ internalUpdateRows(updatedItemIds);
+
+ // Clear all changes.
+ if (rowChanges != null) {
+ rowChanges.clear();
+ }
+ if (updatedItemIds != null) {
+ updatedItemIds.clear();
+ }
+ refreshCache = false;
+ bareItemSetTriggeredSizeChange = false;
+
+ super.beforeClientResponse(initial);
+ }
+
+ private void pushRowData(int firstRowToPush, int numberOfRows,
+ int firstCachedRowIndex, int cacheSize) {
+ Range newRange = Range.withLength(firstRowToPush, numberOfRows);
+ Range cached = Range.withLength(firstCachedRowIndex, cacheSize);
+ Range fullRange = newRange;
+ if (!cached.isEmpty()) {
+ fullRange = newRange.combineWith(cached);
+ }
+
+ List<?> itemIds = container.getItemIds(fullRange.getStart(),
+ fullRange.length());
+
+ JsonArray rows = Json.createArray();
+
+ // Offset the index to match the wanted range.
+ int diff = 0;
+ if (!cached.isEmpty() && newRange.getStart() > cached.getStart()) {
+ diff = cached.length();
+ }
+
+ for (int i = 0; i < newRange.length()
+ && i + diff < itemIds.size(); ++i) {
+ Object itemId = itemIds.get(i + diff);
+
+ Item item = container.getItem(itemId);
+
+ rows.set(i, getRowData(getGrid().getColumns(), itemId, item));
+ }
+ rpc.setRowData(firstRowToPush, rows);
+
+ activeItemHandler.addActiveItems(itemIds);
+ }
+
+ private JsonObject getRowData(Collection<Column> columns, Object itemId,
+ Item item) {
+
+ final JsonObject rowObject = Json.createObject();
+ for (DataGenerator dg : dataGenerators) {
+ dg.generateData(itemId, item, rowObject);
+ }
+
+ return rowObject;
+ }
+
+ /**
+ * Makes the data source available to the given {@link LegacyGrid} component.
+ *
+ * @param component
+ * the remote data grid component to extend
+ * @param columnKeys
+ * the key mapper for columns
+ */
+ public void extend(LegacyGrid component) {
+ super.extend(component);
+ }
+
+ /**
+ * Adds a {@link DataGenerator} for this {@code RpcDataProviderExtension}.
+ * DataGenerators are called when sending row data to client. If given
+ * DataGenerator is already added, this method does nothing.
+ *
+ * @since 7.6
+ * @param generator
+ * generator to add
+ */
+ public void addDataGenerator(DataGenerator generator) {
+ dataGenerators.add(generator);
+ }
+
+ /**
+ * Removes a {@link DataGenerator} from this
+ * {@code RpcDataProviderExtension}. If given DataGenerator is not added to
+ * this data provider, this method does nothing.
+ *
+ * @since 7.6
+ * @param generator
+ * generator to remove
+ */
+ public void removeDataGenerator(DataGenerator generator) {
+ dataGenerators.remove(generator);
+ }
+
+ /**
+ * Informs the client side that new rows have been inserted into the data
+ * source.
+ *
+ * @param index
+ * the index at which new rows have been inserted
+ * @param count
+ * the number of rows inserted at <code>index</code>
+ */
+ private void insertRowData(final int index, final int count) {
+ if (rowChanges == null) {
+ rowChanges = new ArrayList<Runnable>();
+ }
+
+ if (rowChanges.isEmpty()) {
+ markAsDirty();
+ }
+
+ /*
+ * Since all changes should be processed in a consistent order, we don't
+ * send the RPC call immediately. beforeClientResponse will decide
+ * whether to send these or not. Valid situation to not send these is
+ * initial response or bare ItemSetChange event.
+ */
+ rowChanges.add(new Runnable() {
+ @Override
+ public void run() {
+ rpc.insertRowData(index, count);
+ }
+ });
+ }
+
+ /**
+ * Informs the client side that rows have been removed from the data source.
+ *
+ * @param index
+ * the index of the first row removed
+ * @param count
+ * the number of rows removed
+ * @param firstItemId
+ * the item id of the first removed item
+ */
+ private void removeRowData(final int index, final int count) {
+ if (rowChanges == null) {
+ rowChanges = new ArrayList<Runnable>();
+ }
+
+ if (rowChanges.isEmpty()) {
+ markAsDirty();
+ }
+
+ /* See comment in insertRowData */
+ rowChanges.add(new Runnable() {
+ @Override
+ public void run() {
+ rpc.removeRowData(index, count);
+ }
+ });
+ }
+
+ /**
+ * Informs the client side that data of a row has been modified in the data
+ * source.
+ *
+ * @param itemId
+ * the item Id the row that was updated
+ */
+ public void updateRowData(Object itemId) {
+ if (updatedItemIds == null) {
+ updatedItemIds = new LinkedHashSet<Object>();
+ }
+
+ if (updatedItemIds.isEmpty()) {
+ // At least one new item will be updated. Mark as dirty to actually
+ // update before response to client.
+ markAsDirty();
+ }
+
+ updatedItemIds.add(itemId);
+ }
+
+ private void internalUpdateRows(Set<Object> itemIds) {
+ if (itemIds == null || itemIds.isEmpty()) {
+ return;
+ }
+
+ Collection<Object> activeItemIds = activeItemHandler.getActiveItemIds();
+ List<Column> columns = getGrid().getColumns();
+ JsonArray rowData = Json.createArray();
+ int i = 0;
+ for (Object itemId : itemIds) {
+ if (activeItemIds.contains(itemId)) {
+ Item item = container.getItem(itemId);
+ if (item != null) {
+ JsonObject row = getRowData(columns, itemId, item);
+ rowData.set(i++, row);
+ }
+ }
+ }
+ rpc.updateRowData(rowData);
+ }
+
+ /**
+ * Pushes a new version of all the rows in the active cache range.
+ */
+ public void refreshCache() {
+ if (!refreshCache) {
+ refreshCache = true;
+ markAsDirty();
+ }
+ }
+
+ @Override
+ public void setParent(ClientConnector parent) {
+ if (parent == null) {
+ // We're being detached, release various listeners
+ internalDropItems(activeItemHandler.getActiveItemIds());
+
+ if (container instanceof ItemSetChangeNotifier) {
+ ((ItemSetChangeNotifier) container)
+ .removeItemSetChangeListener(itemListener);
+ }
+
+ } else if (!(parent instanceof LegacyGrid)) {
+ throw new IllegalStateException(
+ "Grid is the only accepted parent type");
+ }
+ super.setParent(parent);
+ }
+
+ /**
+ * Informs all DataGenerators than an item id has been dropped.
+ *
+ * @param droppedItemIds
+ * collection of dropped item ids
+ */
+ private void internalDropItems(Collection<Object> droppedItemIds) {
+ for (Object itemId : droppedItemIds) {
+ for (DataGenerator generator : dataGenerators) {
+ generator.destroyData(itemId);
+ }
+ }
+ }
+
+ /**
+ * Informs this data provider that given columns have been removed from
+ * grid.
+ *
+ * @param removedColumns
+ * a list of removed columns
+ */
+ public void columnsRemoved(List<Column> removedColumns) {
+ for (GridValueChangeListener l : activeItemHandler
+ .getValueChangeListeners()) {
+ l.removeColumns(removedColumns);
+ }
+
+ // No need to resend unchanged data. Client will remember the old
+ // columns until next set of rows is sent.
+ }
+
+ /**
+ * Informs this data provider that given columns have been added to grid.
+ *
+ * @param addedColumns
+ * a list of added columns
+ */
+ public void columnsAdded(List<Column> addedColumns) {
+ for (GridValueChangeListener l : activeItemHandler
+ .getValueChangeListeners()) {
+ l.addColumns(addedColumns);
+ }
+
+ // Resend all rows to contain new data.
+ refreshCache();
+ }
+
+ public KeyMapper<Object> getKeyMapper() {
+ return activeItemHandler.keyMapper;
+ }
+
+ protected LegacyGrid getGrid() {
+ return (LegacyGrid) getParent();
+ }
+}
diff --git a/compatibility-server/src/main/java/com/vaadin/ui/LegacyGrid.java b/compatibility-server/src/main/java/com/vaadin/ui/LegacyGrid.java
new file mode 100644
index 0000000000..9a98593b83
--- /dev/null
+++ b/compatibility-server/src/main/java/com/vaadin/ui/LegacyGrid.java
@@ -0,0 +1,7352 @@
+/*
+ * Copyright 2000-2016 Vaadin Ltd.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not
+ * use this file except in compliance with the License. You may obtain a copy of
+ * the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+
+package com.vaadin.ui;
+
+import java.io.Serializable;
+import java.lang.reflect.Method;
+import java.lang.reflect.Type;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.EnumSet;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.Iterator;
+import java.util.LinkedHashMap;
+import java.util.LinkedHashSet;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.Locale;
+import java.util.Map;
+import java.util.Map.Entry;
+import java.util.Set;
+import java.util.logging.Level;
+import java.util.logging.Logger;
+
+import org.jsoup.nodes.Attributes;
+import org.jsoup.nodes.Element;
+import org.jsoup.select.Elements;
+
+import com.vaadin.data.Container;
+import com.vaadin.data.Container.Indexed;
+import com.vaadin.data.Container.ItemSetChangeEvent;
+import com.vaadin.data.Container.ItemSetChangeListener;
+import com.vaadin.data.Container.ItemSetChangeNotifier;
+import com.vaadin.data.Container.PropertySetChangeEvent;
+import com.vaadin.data.Container.PropertySetChangeListener;
+import com.vaadin.data.Container.PropertySetChangeNotifier;
+import com.vaadin.data.Container.Sortable;
+import com.vaadin.data.Item;
+import com.vaadin.data.Property;
+import com.vaadin.data.fieldgroup.DefaultFieldGroupFieldFactory;
+import com.vaadin.data.fieldgroup.FieldGroup;
+import com.vaadin.data.fieldgroup.FieldGroup.CommitException;
+import com.vaadin.data.fieldgroup.FieldGroupFieldFactory;
+import com.vaadin.data.sort.Sort;
+import com.vaadin.data.sort.SortOrder;
+import com.vaadin.data.util.IndexedContainer;
+import com.vaadin.event.ContextClickEvent;
+import com.vaadin.event.ItemClickEvent;
+import com.vaadin.event.ItemClickEvent.ItemClickListener;
+import com.vaadin.event.ItemClickEvent.ItemClickNotifier;
+import com.vaadin.event.SelectionEvent;
+import com.vaadin.event.SelectionEvent.SelectionListener;
+import com.vaadin.event.SelectionEvent.SelectionNotifier;
+import com.vaadin.event.SortEvent;
+import com.vaadin.event.SortEvent.SortListener;
+import com.vaadin.event.SortEvent.SortNotifier;
+import com.vaadin.server.AbstractClientConnector;
+import com.vaadin.server.AbstractExtension;
+import com.vaadin.server.EncodeResult;
+import com.vaadin.server.ErrorMessage;
+import com.vaadin.server.Extension;
+import com.vaadin.server.JsonCodec;
+import com.vaadin.server.KeyMapper;
+import com.vaadin.server.VaadinSession;
+import com.vaadin.server.communication.data.DataGenerator;
+import com.vaadin.server.communication.data.RpcDataProviderExtension;
+import com.vaadin.shared.MouseEventDetails;
+import com.vaadin.shared.data.sort.SortDirection;
+import com.vaadin.shared.ui.grid.EditorClientRpc;
+import com.vaadin.shared.ui.grid.EditorServerRpc;
+import com.vaadin.shared.ui.grid.GridClientRpc;
+import com.vaadin.shared.ui.grid.GridColumnState;
+import com.vaadin.shared.ui.grid.GridConstants;
+import com.vaadin.shared.ui.grid.GridConstants.Section;
+import com.vaadin.shared.ui.grid.GridServerRpc;
+import com.vaadin.shared.ui.grid.GridState;
+import com.vaadin.shared.ui.grid.GridStaticCellType;
+import com.vaadin.shared.ui.grid.GridStaticSectionState;
+import com.vaadin.shared.ui.grid.GridStaticSectionState.CellState;
+import com.vaadin.shared.ui.grid.GridStaticSectionState.RowState;
+import com.vaadin.shared.ui.grid.HeightMode;
+import com.vaadin.shared.ui.grid.ScrollDestination;
+import com.vaadin.shared.ui.grid.selection.MultiSelectionModelServerRpc;
+import com.vaadin.shared.ui.grid.selection.MultiSelectionModelState;
+import com.vaadin.shared.ui.grid.selection.SingleSelectionModelServerRpc;
+import com.vaadin.shared.ui.grid.selection.SingleSelectionModelState;
+import com.vaadin.shared.util.SharedUtil;
+import com.vaadin.ui.declarative.DesignAttributeHandler;
+import com.vaadin.ui.declarative.DesignContext;
+import com.vaadin.ui.declarative.DesignException;
+import com.vaadin.ui.declarative.DesignFormatter;
+import com.vaadin.ui.renderers.HtmlRenderer;
+import com.vaadin.ui.renderers.Renderer;
+import com.vaadin.ui.renderers.TextRenderer;
+import com.vaadin.util.ReflectTools;
+import com.vaadin.v7.data.Validator.InvalidValueException;
+import com.vaadin.v7.data.util.converter.LegacyConverter;
+import com.vaadin.v7.data.util.converter.LegacyConverterUtil;
+import com.vaadin.v7.ui.LegacyCheckBox;
+import com.vaadin.v7.ui.LegacyField;
+
+import elemental.json.Json;
+import elemental.json.JsonObject;
+import elemental.json.JsonValue;
+
+/**
+ * A grid component for displaying tabular data.
+ * <p>
+ * Grid is always bound to a {@link Container.Indexed}, but is not a
+ * {@code Container} of any kind in of itself. The contents of the given
+ * Container is displayed with the help of {@link Renderer Renderers}.
+ *
+ * <h3 id="grid-headers-and-footers">Headers and Footers</h3>
+ * <p>
+ *
+ *
+ * <h3 id="grid-converters-and-renderers">Converters and Renderers</h3>
+ * <p>
+ * Each column has its own {@link Renderer} that displays data into something
+ * that can be displayed in the browser. That data is first converted with a
+ * {@link com.vaadin.v7.data.util.converter.LegacyConverter Converter} into
+ * something that the Renderer can process. This can also be an implicit step -
+ * if a column has a simple data type, like a String, no explicit assignment is
+ * needed.
+ * <p>
+ * Usually a renderer takes some kind of object, and converts it into a
+ * HTML-formatted string.
+ * <p>
+ * <code><pre>
+ * Grid grid = new Grid(myContainer);
+ * Column column = grid.getColumn(STRING_DATE_PROPERTY);
+ * column.setConverter(new StringToDateConverter());
+ * column.setRenderer(new MyColorfulDateRenderer());
+ * </pre></code>
+ *
+ * <h3 id="grid-lazyloading">Lazy Loading</h3>
+ * <p>
+ * The data is accessed as it is needed by Grid and not any sooner. In other
+ * words, if the given Container is huge, but only the first few rows are
+ * displayed to the user, only those (and a few more, for caching purposes) are
+ * accessed.
+ *
+ * <h3 id="grid-selection-modes-and-models">Selection Modes and Models</h3>
+ * <p>
+ * Grid supports three selection <em>{@link SelectionMode modes}</em> (single,
+ * multi, none), and comes bundled with one <em>{@link SelectionModel
+ * model}</em> for each of the modes. The distinction between a selection mode
+ * and selection model is as follows: a <em>mode</em> essentially says whether
+ * you can have one, many or no rows selected. The model, however, has the
+ * behavioral details of each. A single selection model may require that the
+ * user deselects one row before selecting another one. A variant of a
+ * multiselect might have a configurable maximum of rows that may be selected.
+ * And so on.
+ * <p>
+ * <code><pre>
+ * Grid grid = new Grid(myContainer);
+ *
+ * // uses the bundled SingleSelectionModel class
+ * grid.setSelectionMode(SelectionMode.SINGLE);
+ *
+ * // changes the behavior to a custom selection model
+ * grid.setSelectionModel(new MyTwoSelectionModel());
+ * </pre></code>
+ *
+ * @since 7.4
+ * @author Vaadin Ltd
+ */
+public class LegacyGrid extends AbstractFocusable implements SelectionNotifier,
+ SortNotifier, SelectiveRenderer, ItemClickNotifier {
+
+ /**
+ * An event listener for column visibility change events in the Grid.
+ *
+ * @since 7.5.0
+ */
+ public interface ColumnVisibilityChangeListener extends Serializable {
+ /**
+ * Called when a column has become hidden or unhidden.
+ *
+ * @param event
+ */
+ void columnVisibilityChanged(ColumnVisibilityChangeEvent event);
+ }
+
+ /**
+ * An event that is fired when a column's visibility changes.
+ *
+ * @since 7.5.0
+ */
+ public static class ColumnVisibilityChangeEvent extends Component.Event {
+
+ private final Column column;
+ private final boolean userOriginated;
+ private final boolean hidden;
+
+ /**
+ * Constructor for a column visibility change event.
+ *
+ * @param source
+ * the grid from which this event originates
+ * @param column
+ * the column that changed its visibility
+ * @param hidden
+ * <code>true</code> if the column was hidden,
+ * <code>false</code> if it became visible
+ * @param isUserOriginated
+ * <code>true</code> iff the event was triggered by an UI
+ * interaction
+ */
+ public ColumnVisibilityChangeEvent(LegacyGrid source, Column column,
+ boolean hidden, boolean isUserOriginated) {
+ super(source);
+ this.column = column;
+ this.hidden = hidden;
+ userOriginated = isUserOriginated;
+ }
+
+ /**
+ * Gets the column that became hidden or visible.
+ *
+ * @return the column that became hidden or visible.
+ * @see Column#isHidden()
+ */
+ public Column getColumn() {
+ return column;
+ }
+
+ /**
+ * Was the column set hidden or visible.
+ *
+ * @return <code>true</code> if the column was hidden <code>false</code>
+ * if it was set visible
+ */
+ public boolean isHidden() {
+ return hidden;
+ }
+
+ /**
+ * Returns <code>true</code> if the column reorder was done by the user,
+ * <code>false</code> if not and it was triggered by server side code.
+ *
+ * @return <code>true</code> if event is a result of user interaction
+ */
+ public boolean isUserOriginated() {
+ return userOriginated;
+ }
+ }
+
+ /**
+ * A callback interface for generating details for a particular row in Grid.
+ *
+ * @since 7.5.0
+ * @author Vaadin Ltd
+ * @see DetailsGenerator#NULL
+ */
+ public interface DetailsGenerator extends Serializable {
+
+ /** A details generator that provides no details */
+ public DetailsGenerator NULL = new DetailsGenerator() {
+ @Override
+ public Component getDetails(RowReference rowReference) {
+ return null;
+ }
+ };
+
+ /**
+ * This method is called for whenever a details row needs to be shown on
+ * the client. Grid removes all of its references to details components
+ * when they are no longer displayed on the client-side and will
+ * re-request once needed again.
+ * <p>
+ * <em>Note:</em> If a component gets generated, it may not be manually
+ * attached anywhere. The same details component can not be displayed
+ * for multiple different rows.
+ *
+ * @param rowReference
+ * the reference for the row for which to generate details
+ * @return the details for the given row, or <code>null</code> to leave
+ * the details empty.
+ */
+ Component getDetails(RowReference rowReference);
+ }
+
+ /**
+ * A class that manages details components by calling
+ * {@link DetailsGenerator} as needed. Details components are attached by
+ * this class when the {@link RpcDataProviderExtension} is sending data to
+ * the client. Details components are detached and forgotten when client
+ * informs that it has dropped the corresponding item.
+ *
+ * @since 7.6.1
+ */
+ public final static class DetailComponentManager
+ extends AbstractGridExtension implements DataGenerator {
+
+ /**
+ * The user-defined details generator.
+ *
+ * @see #setDetailsGenerator(DetailsGenerator)
+ */
+ private DetailsGenerator detailsGenerator;
+
+ /**
+ * This map represents all details that are currently visible on the
+ * client. Details components get destroyed once they scroll out of
+ * view.
+ */
+ private final Map<Object, Component> itemIdToDetailsComponent = new HashMap<>();
+
+ /**
+ * Set of item ids that got <code>null</code> from DetailsGenerator when
+ * {@link DetailsGenerator#getDetails(RowReference)} was called.
+ */
+ private final Set<Object> emptyDetails = new HashSet<>();
+
+ /**
+ * Set of item IDs for all open details rows. Contains even the ones
+ * that are not currently visible on the client.
+ */
+ private final Set<Object> openDetails = new HashSet<>();
+
+ public DetailComponentManager(LegacyGrid grid) {
+ this(grid, DetailsGenerator.NULL);
+ }
+
+ public DetailComponentManager(LegacyGrid grid,
+ DetailsGenerator detailsGenerator) {
+ super(grid);
+ setDetailsGenerator(detailsGenerator);
+ }
+
+ /**
+ * Creates a details component with the help of the user-defined
+ * {@link DetailsGenerator}.
+ * <p>
+ * This method attaches created components to the parent {@link LegacyGrid}.
+ *
+ * @param itemId
+ * the item id for which to create the details component.
+ * @throws IllegalStateException
+ * if the current details generator provides a component
+ * that was manually attached.
+ */
+ private void createDetails(Object itemId) throws IllegalStateException {
+ assert itemId != null : "itemId was null";
+
+ if (itemIdToDetailsComponent.containsKey(itemId)
+ || emptyDetails.contains(itemId)) {
+ // Don't overwrite existing components
+ return;
+ }
+
+ RowReference rowReference = new RowReference(getParentGrid());
+ rowReference.set(itemId);
+
+ DetailsGenerator detailsGenerator = getParentGrid()
+ .getDetailsGenerator();
+ Component details = detailsGenerator.getDetails(rowReference);
+ if (details != null) {
+ if (details.getParent() != null) {
+ String name = detailsGenerator.getClass().getName();
+ throw new IllegalStateException(
+ name + " generated a details component that already "
+ + "was attached. (itemId: " + itemId
+ + ", component: " + details + ")");
+ }
+
+ itemIdToDetailsComponent.put(itemId, details);
+
+ addComponentToGrid(details);
+
+ assert !emptyDetails.contains(itemId) : "Bookeeping thinks "
+ + "itemId is empty even though we just created a "
+ + "component for it (" + itemId + ")";
+ } else {
+ emptyDetails.add(itemId);
+ }
+
+ }
+
+ /**
+ * Destroys a details component correctly.
+ * <p>
+ * This method will detach the component from parent {@link LegacyGrid}.
+ *
+ * @param itemId
+ * the item id for which to destroy the details component
+ */
+ private void destroyDetails(Object itemId) {
+ emptyDetails.remove(itemId);
+
+ Component removedComponent = itemIdToDetailsComponent
+ .remove(itemId);
+ if (removedComponent == null) {
+ return;
+ }
+
+ removeComponentFromGrid(removedComponent);
+ }
+
+ /**
+ * Recreates all visible details components.
+ */
+ public void refreshDetails() {
+ Set<Object> visibleItemIds = new HashSet<>(
+ itemIdToDetailsComponent.keySet());
+ for (Object itemId : visibleItemIds) {
+ destroyDetails(itemId);
+ createDetails(itemId);
+ refreshRow(itemId);
+ }
+ }
+
+ /**
+ * Sets details visiblity status of given item id.
+ *
+ * @param itemId
+ * item id to set
+ * @param visible
+ * <code>true</code> if visible; <code>false</code> if not
+ */
+ public void setDetailsVisible(Object itemId, boolean visible) {
+ if ((visible && openDetails.contains(itemId))
+ || (!visible && !openDetails.contains(itemId))) {
+ return;
+ }
+
+ if (visible) {
+ openDetails.add(itemId);
+ refreshRow(itemId);
+ } else {
+ openDetails.remove(itemId);
+ destroyDetails(itemId);
+ refreshRow(itemId);
+ }
+ }
+
+ @Override
+ public void generateData(Object itemId, Item item, JsonObject rowData) {
+ // DetailComponentManager should not send anything if details
+ // generator is the default null version.
+ if (openDetails.contains(itemId)
+ && !detailsGenerator.equals(DetailsGenerator.NULL)) {
+ // Double check to be sure details component exists.
+ createDetails(itemId);
+
+ Component detailsComponent = itemIdToDetailsComponent
+ .get(itemId);
+ rowData.put(GridState.JSONKEY_DETAILS_VISIBLE,
+ (detailsComponent != null
+ ? detailsComponent.getConnectorId() : ""));
+ }
+ }
+
+ @Override
+ public void destroyData(Object itemId) {
+ if (openDetails.contains(itemId)) {
+ destroyDetails(itemId);
+ }
+ }
+
+ /**
+ * Sets a new details generator for row details.
+ * <p>
+ * The currently opened row details will be re-rendered.
+ *
+ * @param detailsGenerator
+ * the details generator to set
+ * @throws IllegalArgumentException
+ * if detailsGenerator is <code>null</code>;
+ */
+ public void setDetailsGenerator(DetailsGenerator detailsGenerator)
+ throws IllegalArgumentException {
+ if (detailsGenerator == null) {
+ throw new IllegalArgumentException(
+ "Details generator may not be null");
+ } else if (detailsGenerator == this.detailsGenerator) {
+ return;
+ }
+
+ this.detailsGenerator = detailsGenerator;
+
+ refreshDetails();
+ }
+
+ /**
+ * Gets the current details generator for row details.
+ *
+ * @return the detailsGenerator the current details generator
+ */
+ public DetailsGenerator getDetailsGenerator() {
+ return detailsGenerator;
+ }
+
+ /**
+ * Checks whether details are visible for the given item.
+ *
+ * @param itemId
+ * the id of the item for which to check details visibility
+ * @return <code>true</code> iff the details are visible
+ */
+ public boolean isDetailsVisible(Object itemId) {
+ return openDetails.contains(itemId);
+ }
+ }
+
+ /**
+ * Custom field group that allows finding property types before an item has
+ * been bound.
+ */
+ private final class CustomFieldGroup extends FieldGroup {
+
+ public CustomFieldGroup() {
+ setFieldFactory(EditorFieldFactory.get());
+ }
+
+ @Override
+ protected Class<?> getPropertyType(Object propertyId)
+ throws BindException {
+ if (getItemDataSource() == null) {
+ return datasource.getType(propertyId);
+ } else {
+ return super.getPropertyType(propertyId);
+ }
+ }
+
+ @Override
+ protected <T extends LegacyField> T build(String caption,
+ Class<?> dataType, Class<T> fieldType) throws BindException {
+ T field = super.build(caption, dataType, fieldType);
+ if (field instanceof LegacyCheckBox) {
+ field.setCaption(null);
+ }
+ return field;
+ }
+ }
+
+ /**
+ * Field factory used by default in the editor.
+ *
+ * Aims to fields of suitable type and with suitable size for use in the
+ * editor row.
+ */
+ public static class EditorFieldFactory
+ extends DefaultFieldGroupFieldFactory {
+ private static final EditorFieldFactory INSTANCE = new EditorFieldFactory();
+
+ protected EditorFieldFactory() {
+ }
+
+ /**
+ * Returns the singleton instance
+ *
+ * @return the singleton instance
+ */
+ public static EditorFieldFactory get() {
+ return INSTANCE;
+ }
+
+ @Override
+ public <T extends LegacyField> T createField(Class<?> type,
+ Class<T> fieldType) {
+ T f = super.createField(type, fieldType);
+ if (f != null) {
+ f.setWidth("100%");
+ }
+ return f;
+ }
+
+ @Override
+ protected AbstractSelect createCompatibleSelect(
+ Class<? extends AbstractSelect> fieldType) {
+ if (anySelect(fieldType)) {
+ return super.createCompatibleSelect(ComboBox.class);
+ }
+ return super.createCompatibleSelect(fieldType);
+ }
+
+ @Override
+ protected void populateWithEnumData(AbstractSelect select,
+ Class<? extends Enum> enumClass) {
+ // Use enums directly and the EnumToStringConverter to be consistent
+ // with what is shown in the Grid
+ @SuppressWarnings("unchecked")
+ EnumSet<?> enumSet = EnumSet.allOf(enumClass);
+ for (Object r : enumSet) {
+ select.addItem(r);
+ }
+ }
+ }
+
+ /**
+ * Error handler for the editor
+ */
+ public interface EditorErrorHandler extends Serializable {
+
+ /**
+ * Called when an exception occurs while the editor row is being saved
+ *
+ * @param event
+ * An event providing more information about the error
+ */
+ void commitError(CommitErrorEvent event);
+ }
+
+ /**
+ * ContextClickEvent for the Grid Component.
+ *
+ * @since 7.6
+ */
+ public static class GridContextClickEvent extends ContextClickEvent {
+
+ private final Object itemId;
+ private final int rowIndex;
+ private final Object propertyId;
+ private final Section section;
+
+ public GridContextClickEvent(LegacyGrid source,
+ MouseEventDetails mouseEventDetails, Section section,
+ int rowIndex, Object itemId, Object propertyId) {
+ super(source, mouseEventDetails);
+ this.itemId = itemId;
+ this.propertyId = propertyId;
+ this.section = section;
+ this.rowIndex = rowIndex;
+ }
+
+ /**
+ * Returns the item id of context clicked row.
+ *
+ * @return item id of clicked row; <code>null</code> if header or footer
+ */
+ public Object getItemId() {
+ return itemId;
+ }
+
+ /**
+ * Returns property id of clicked column.
+ *
+ * @return property id
+ */
+ public Object getPropertyId() {
+ return propertyId;
+ }
+
+ /**
+ * Return the clicked section of Grid.
+ *
+ * @return section of grid
+ */
+ public Section getSection() {
+ return section;
+ }
+
+ /**
+ * Returns the clicked row index relative to Grid section. In the body
+ * of the Grid the index is the item index in the Container. Header and
+ * Footer rows for index can be fetched with
+ * {@link LegacyGrid#getHeaderRow(int)} and {@link LegacyGrid#getFooterRow(int)}.
+ *
+ * @return row index in section
+ */
+ public int getRowIndex() {
+ return rowIndex;
+ }
+
+ @Override
+ public LegacyGrid getComponent() {
+ return (LegacyGrid) super.getComponent();
+ }
+ }
+
+ /**
+ * An event which is fired when saving the editor fails
+ */
+ public static class CommitErrorEvent extends Component.Event {
+
+ private CommitException cause;
+
+ private Set<Column> errorColumns = new HashSet<>();
+
+ private String userErrorMessage;
+
+ public CommitErrorEvent(LegacyGrid grid, CommitException cause) {
+ super(grid);
+ this.cause = cause;
+ userErrorMessage = cause.getLocalizedMessage();
+ }
+
+ /**
+ * Retrieves the cause of the failure
+ *
+ * @return the cause of the failure
+ */
+ public CommitException getCause() {
+ return cause;
+ }
+
+ @Override
+ public LegacyGrid getComponent() {
+ return (LegacyGrid) super.getComponent();
+ }
+
+ /**
+ * Checks if validation exceptions caused this error
+ *
+ * @return true if the problem was caused by a validation error
+ */
+ public boolean isValidationFailure() {
+ return cause.getCause() instanceof InvalidValueException;
+ }
+
+ /**
+ * Marks that an error indicator should be shown for the editor of a
+ * column.
+ *
+ * @param column
+ * the column to show an error for
+ */
+ public void addErrorColumn(Column column) {
+ errorColumns.add(column);
+ }
+
+ /**
+ * Gets all the columns that have been marked as erroneous.
+ *
+ * @return an umodifiable collection of erroneous columns
+ */
+ public Collection<Column> getErrorColumns() {
+ return Collections.unmodifiableCollection(errorColumns);
+ }
+
+ /**
+ * Gets the error message to show to the user.
+ *
+ * @return error message to show
+ */
+ public String getUserErrorMessage() {
+ return userErrorMessage;
+ }
+
+ /**
+ * Sets the error message to show to the user.
+ *
+ * @param userErrorMessage
+ * the user error message to set
+ */
+ public void setUserErrorMessage(String userErrorMessage) {
+ this.userErrorMessage = userErrorMessage;
+ }
+
+ }
+
+ /**
+ * An event listener for column reorder events in the Grid.
+ *
+ * @since 7.5.0
+ */
+ public interface ColumnReorderListener extends Serializable {
+
+ /**
+ * Called when the columns of the grid have been reordered.
+ *
+ * @param event
+ * An event providing more information
+ */
+ void columnReorder(ColumnReorderEvent event);
+ }
+
+ /**
+ * An event that is fired when the columns are reordered.
+ *
+ * @since 7.5.0
+ */
+ public static class ColumnReorderEvent extends Component.Event {
+
+ private final boolean userOriginated;
+
+ /**
+ *
+ * @param source
+ * the grid where the event originated from
+ * @param userOriginated
+ * <code>true</code> if event is a result of user
+ * interaction, <code>false</code> if from API call
+ */
+ public ColumnReorderEvent(LegacyGrid source, boolean userOriginated) {
+ super(source);
+ this.userOriginated = userOriginated;
+ }
+
+ /**
+ * Returns <code>true</code> if the column reorder was done by the user,
+ * <code>false</code> if not and it was triggered by server side code.
+ *
+ * @return <code>true</code> if event is a result of user interaction
+ */
+ public boolean isUserOriginated() {
+ return userOriginated;
+ }
+
+ }
+
+ /**
+ * An event listener for column resize events in the Grid.
+ *
+ * @since 7.6
+ */
+ public interface ColumnResizeListener extends Serializable {
+
+ /**
+ * Called when the columns of the grid have been resized.
+ *
+ * @param event
+ * An event providing more information
+ */
+ void columnResize(ColumnResizeEvent event);
+ }
+
+ /**
+ * An event that is fired when a column is resized, either programmatically
+ * or by the user.
+ *
+ * @since 7.6
+ */
+ public static class ColumnResizeEvent extends Component.Event {
+
+ private final Column column;
+ private final boolean userOriginated;
+
+ /**
+ *
+ * @param source
+ * the grid where the event originated from
+ * @param userOriginated
+ * <code>true</code> if event is a result of user
+ * interaction, <code>false</code> if from API call
+ */
+ public ColumnResizeEvent(LegacyGrid source, Column column,
+ boolean userOriginated) {
+ super(source);
+ this.column = column;
+ this.userOriginated = userOriginated;
+ }
+
+ /**
+ * Returns the column that was resized.
+ *
+ * @return the resized column.
+ */
+ public Column getColumn() {
+ return column;
+ }
+
+ /**
+ * Returns <code>true</code> if the column resize was done by the user,
+ * <code>false</code> if not and it was triggered by server side code.
+ *
+ * @return <code>true</code> if event is a result of user interaction
+ */
+ public boolean isUserOriginated() {
+ return userOriginated;
+ }
+
+ }
+
+ /**
+ * Interface for an editor event listener
+ */
+ public interface EditorListener extends Serializable {
+
+ public static final Method EDITOR_OPEN_METHOD = ReflectTools.findMethod(
+ EditorListener.class, "editorOpened", EditorOpenEvent.class);
+ public static final Method EDITOR_MOVE_METHOD = ReflectTools.findMethod(
+ EditorListener.class, "editorMoved", EditorMoveEvent.class);
+ public static final Method EDITOR_CLOSE_METHOD = ReflectTools
+ .findMethod(EditorListener.class, "editorClosed",
+ EditorCloseEvent.class);
+
+ /**
+ * Called when an editor is opened
+ *
+ * @param e
+ * an editor open event object
+ */
+ public void editorOpened(EditorOpenEvent e);
+
+ /**
+ * Called when an editor is reopened without closing it first
+ *
+ * @param e
+ * an editor move event object
+ */
+ public void editorMoved(EditorMoveEvent e);
+
+ /**
+ * Called when an editor is closed
+ *
+ * @param e
+ * an editor close event object
+ */
+ public void editorClosed(EditorCloseEvent e);
+
+ }
+
+ /**
+ * Base class for editor related events
+ */
+ public static abstract class EditorEvent extends Component.Event {
+
+ private Object itemID;
+
+ protected EditorEvent(LegacyGrid source, Object itemID) {
+ super(source);
+ this.itemID = itemID;
+ }
+
+ /**
+ * Get the item (row) for which this editor was opened
+ */
+ public Object getItem() {
+ return itemID;
+ }
+
+ }
+
+ /**
+ * This event gets fired when an editor is opened
+ */
+ public static class EditorOpenEvent extends EditorEvent {
+
+ public EditorOpenEvent(LegacyGrid source, Object itemID) {
+ super(source, itemID);
+ }
+ }
+
+ /**
+ * This event gets fired when an editor is opened while another row is being
+ * edited (i.e. editor focus moves elsewhere)
+ */
+ public static class EditorMoveEvent extends EditorEvent {
+
+ public EditorMoveEvent(LegacyGrid source, Object itemID) {
+ super(source, itemID);
+ }
+ }
+
+ /**
+ * This event gets fired when an editor is dismissed or closed by other
+ * means.
+ */
+ public static class EditorCloseEvent extends EditorEvent {
+
+ public EditorCloseEvent(LegacyGrid source, Object itemID) {
+ super(source, itemID);
+ }
+ }
+
+ /**
+ * Default error handler for the editor
+ *
+ */
+ public class DefaultEditorErrorHandler implements EditorErrorHandler {
+
+ @Override
+ public void commitError(CommitErrorEvent event) {
+ Map<LegacyField<?>, InvalidValueException> invalidFields = event
+ .getCause().getInvalidFields();
+
+ if (!invalidFields.isEmpty()) {
+ Object firstErrorPropertyId = null;
+ LegacyField<?> firstErrorField = null;
+
+ FieldGroup fieldGroup = event.getCause().getFieldGroup();
+ for (Column column : getColumns()) {
+ Object propertyId = column.getPropertyId();
+ LegacyField<?> field = fieldGroup.getField(propertyId);
+ if (invalidFields.keySet().contains(field)) {
+ event.addErrorColumn(column);
+
+ if (firstErrorPropertyId == null) {
+ firstErrorPropertyId = propertyId;
+ firstErrorField = field;
+ }
+ }
+ }
+
+ /*
+ * Validation error, show first failure as
+ * "<Column header>: <message>"
+ */
+ String caption = getColumn(firstErrorPropertyId)
+ .getHeaderCaption();
+ String message = invalidFields.get(firstErrorField)
+ .getLocalizedMessage();
+
+ event.setUserErrorMessage(caption + ": " + message);
+ } else {
+ com.vaadin.server.ErrorEvent.findErrorHandler(LegacyGrid.this).error(
+ new ConnectorErrorEvent(LegacyGrid.this, event.getCause()));
+ }
+ }
+
+ private Object getFirstPropertyId(FieldGroup fieldGroup,
+ Set<LegacyField<?>> keySet) {
+ for (Column c : getColumns()) {
+ Object propertyId = c.getPropertyId();
+ LegacyField<?> f = fieldGroup.getField(propertyId);
+ if (keySet.contains(f)) {
+ return propertyId;
+ }
+ }
+ return null;
+ }
+ }
+
+ /**
+ * Selection modes representing built-in {@link SelectionModel
+ * SelectionModels} that come bundled with {@link LegacyGrid}.
+ * <p>
+ * Passing one of these enums into
+ * {@link LegacyGrid#setSelectionMode(SelectionMode)} is equivalent to calling
+ * {@link LegacyGrid#setSelectionModel(SelectionModel)} with one of the built-in
+ * implementations of {@link SelectionModel}.
+ *
+ * @see LegacyGrid#setSelectionMode(SelectionMode)
+ * @see LegacyGrid#setSelectionModel(SelectionModel)
+ */
+ public enum SelectionMode {
+ /** A SelectionMode that maps to {@link SingleSelectionModel} */
+ SINGLE {
+ @Override
+ protected SelectionModel createModel() {
+ return new SingleSelectionModel();
+ }
+
+ },
+
+ /** A SelectionMode that maps to {@link MultiSelectionModel} */
+ MULTI {
+ @Override
+ protected SelectionModel createModel() {
+ return new MultiSelectionModel();
+ }
+ },
+
+ /** A SelectionMode that maps to {@link NoSelectionModel} */
+ NONE {
+ @Override
+ protected SelectionModel createModel() {
+ return new NoSelectionModel();
+ }
+ };
+
+ protected abstract SelectionModel createModel();
+ }
+
+ /**
+ * The server-side interface that controls Grid's selection state.
+ * SelectionModel should extend {@link AbstractGridExtension}.
+ */
+ public interface SelectionModel extends Serializable, Extension {
+ /**
+ * Checks whether an item is selected or not.
+ *
+ * @param itemId
+ * the item id to check for
+ * @return <code>true</code> iff the item is selected
+ */
+ boolean isSelected(Object itemId);
+
+ /**
+ * Returns a collection of all the currently selected itemIds.
+ *
+ * @return a collection of all the currently selected itemIds
+ */
+ Collection<Object> getSelectedRows();
+
+ /**
+ * Injects the current {@link LegacyGrid} instance into the SelectionModel.
+ * This method should usually call the extend method of
+ * {@link AbstractExtension}.
+ * <p>
+ * <em>Note:</em> This method should not be called manually.
+ *
+ * @param grid
+ * the Grid in which the SelectionModel currently is, or
+ * <code>null</code> when a selection model is being detached
+ * from a Grid.
+ */
+ void setGrid(LegacyGrid grid);
+
+ /**
+ * Resets the SelectiomModel to an initial state.
+ * <p>
+ * Most often this means that the selection state is cleared, but
+ * implementations are free to interpret the "initial state" as they
+ * wish. Some, for example, may want to keep the first selected item as
+ * selected.
+ */
+ void reset();
+
+ /**
+ * A SelectionModel that supports multiple selections to be made.
+ * <p>
+ * This interface has a contract of having the same behavior, no matter
+ * how the selection model is interacted with. In other words, if
+ * something is forbidden to do in e.g. the user interface, it must also
+ * be forbidden to do in the server-side and client-side APIs.
+ */
+ public interface Multi extends SelectionModel {
+
+ /**
+ * Marks items as selected.
+ * <p>
+ * This method does not clear any previous selection state, only
+ * adds to it.
+ *
+ * @param itemIds
+ * the itemId(s) to mark as selected
+ * @return <code>true</code> if the selection state changed.
+ * <code>false</code> if all the given itemIds already were
+ * selected
+ * @throws IllegalArgumentException
+ * if the <code>itemIds</code> varargs array is
+ * <code>null</code> or given itemIds don't exist in the
+ * container of Grid
+ * @see #deselect(Object...)
+ */
+ boolean select(Object... itemIds) throws IllegalArgumentException;
+
+ /**
+ * Marks items as selected.
+ * <p>
+ * This method does not clear any previous selection state, only
+ * adds to it.
+ *
+ * @param itemIds
+ * the itemIds to mark as selected
+ * @return <code>true</code> if the selection state changed.
+ * <code>false</code> if all the given itemIds already were
+ * selected
+ * @throws IllegalArgumentException
+ * if <code>itemIds</code> is <code>null</code> or given
+ * itemIds don't exist in the container of Grid
+ * @see #deselect(Collection)
+ */
+ boolean select(Collection<?> itemIds)
+ throws IllegalArgumentException;
+
+ /**
+ * Marks items as deselected.
+ *
+ * @param itemIds
+ * the itemId(s) to remove from being selected
+ * @return <code>true</code> if the selection state changed.
+ * <code>false</code> if none the given itemIds were
+ * selected previously
+ * @throws IllegalArgumentException
+ * if the <code>itemIds</code> varargs array is
+ * <code>null</code>
+ * @see #select(Object...)
+ */
+ boolean deselect(Object... itemIds) throws IllegalArgumentException;
+
+ /**
+ * Marks items as deselected.
+ *
+ * @param itemIds
+ * the itemId(s) to remove from being selected
+ * @return <code>true</code> if the selection state changed.
+ * <code>false</code> if none the given itemIds were
+ * selected previously
+ * @throws IllegalArgumentException
+ * if <code>itemIds</code> is <code>null</code>
+ * @see #select(Collection)
+ */
+ boolean deselect(Collection<?> itemIds)
+ throws IllegalArgumentException;
+
+ /**
+ * Marks all the items in the current Container as selected
+ *
+ * @return <code>true</code> iff some items were previously not
+ * selected
+ * @see #deselectAll()
+ */
+ boolean selectAll();
+
+ /**
+ * Marks all the items in the current Container as deselected
+ *
+ * @return <code>true</code> iff some items were previously selected
+ * @see #selectAll()
+ */
+ boolean deselectAll();
+
+ /**
+ * Marks items as selected while deselecting all items not in the
+ * given Collection.
+ *
+ * @param itemIds
+ * the itemIds to mark as selected
+ * @return <code>true</code> if the selection state changed.
+ * <code>false</code> if all the given itemIds already were
+ * selected
+ * @throws IllegalArgumentException
+ * if <code>itemIds</code> is <code>null</code> or given
+ * itemIds don't exist in the container of Grid
+ */
+ boolean setSelected(Collection<?> itemIds)
+ throws IllegalArgumentException;
+
+ /**
+ * Marks items as selected while deselecting all items not in the
+ * varargs array.
+ *
+ * @param itemIds
+ * the itemIds to mark as selected
+ * @return <code>true</code> if the selection state changed.
+ * <code>false</code> if all the given itemIds already were
+ * selected
+ * @throws IllegalArgumentException
+ * if the <code>itemIds</code> varargs array is
+ * <code>null</code> or given itemIds don't exist in the
+ * container of Grid
+ */
+ boolean setSelected(Object... itemIds)
+ throws IllegalArgumentException;
+ }
+
+ /**
+ * A SelectionModel that supports for only single rows to be selected at
+ * a time.
+ * <p>
+ * This interface has a contract of having the same behavior, no matter
+ * how the selection model is interacted with. In other words, if
+ * something is forbidden to do in e.g. the user interface, it must also
+ * be forbidden to do in the server-side and client-side APIs.
+ */
+ public interface Single extends SelectionModel {
+
+ /**
+ * Marks an item as selected.
+ *
+ * @param itemId
+ * the itemId to mark as selected; <code>null</code> for
+ * deselect
+ * @return <code>true</code> if the selection state changed.
+ * <code>false</code> if the itemId already was selected
+ * @throws IllegalStateException
+ * if the selection was illegal. One such reason might
+ * be that the given id was null, indicating a deselect,
+ * but implementation doesn't allow deselecting.
+ * re-selecting something
+ * @throws IllegalArgumentException
+ * if given itemId does not exist in the container of
+ * Grid
+ */
+ boolean select(Object itemId)
+ throws IllegalStateException, IllegalArgumentException;
+
+ /**
+ * Gets the item id of the currently selected item.
+ *
+ * @return the item id of the currently selected item, or
+ * <code>null</code> if nothing is selected
+ */
+ Object getSelectedRow();
+
+ /**
+ * Sets whether it's allowed to deselect the selected row through
+ * the UI. Deselection is allowed by default.
+ *
+ * @param deselectAllowed
+ * <code>true</code> if the selected row can be
+ * deselected without selecting another row instead;
+ * otherwise <code>false</code>.
+ */
+ public void setDeselectAllowed(boolean deselectAllowed);
+
+ /**
+ * Sets whether it's allowed to deselect the selected row through
+ * the UI.
+ *
+ * @return <code>true</code> if deselection is allowed; otherwise
+ * <code>false</code>
+ */
+ public boolean isDeselectAllowed();
+ }
+
+ /**
+ * A SelectionModel that does not allow for rows to be selected.
+ * <p>
+ * This interface has a contract of having the same behavior, no matter
+ * how the selection model is interacted with. In other words, if the
+ * developer is unable to select something programmatically, it is not
+ * allowed for the end-user to select anything, either.
+ */
+ public interface None extends SelectionModel {
+
+ /**
+ * {@inheritDoc}
+ *
+ * @return always <code>false</code>.
+ */
+ @Override
+ public boolean isSelected(Object itemId);
+
+ /**
+ * {@inheritDoc}
+ *
+ * @return always an empty collection.
+ */
+ @Override
+ public Collection<Object> getSelectedRows();
+ }
+ }
+
+ /**
+ * A base class for SelectionModels that contains some of the logic that is
+ * reusable.
+ */
+ public static abstract class AbstractSelectionModel extends
+ AbstractGridExtension implements SelectionModel, DataGenerator {
+ protected final LinkedHashSet<Object> selection = new LinkedHashSet<>();
+
+ @Override
+ public boolean isSelected(final Object itemId) {
+ return selection.contains(itemId);
+ }
+
+ @Override
+ public Collection<Object> getSelectedRows() {
+ return new ArrayList<>(selection);
+ }
+
+ @Override
+ public void setGrid(final LegacyGrid grid) {
+ if (grid != null) {
+ extend(grid);
+ }
+ }
+
+ /**
+ * Sanity check for existence of item id.
+ *
+ * @param itemId
+ * item id to be selected / deselected
+ *
+ * @throws IllegalArgumentException
+ * if item Id doesn't exist in the container of Grid
+ */
+ protected void checkItemIdExists(Object itemId)
+ throws IllegalArgumentException {
+ if (!getParentGrid().getContainerDataSource().containsId(itemId)) {
+ throw new IllegalArgumentException("Given item id (" + itemId
+ + ") does not exist in the container");
+ }
+ }
+
+ /**
+ * Sanity check for existence of item ids in given collection.
+ *
+ * @param itemIds
+ * item id collection to be selected / deselected
+ *
+ * @throws IllegalArgumentException
+ * if at least one item id doesn't exist in the container of
+ * Grid
+ */
+ protected void checkItemIdsExist(Collection<?> itemIds)
+ throws IllegalArgumentException {
+ for (Object itemId : itemIds) {
+ checkItemIdExists(itemId);
+ }
+ }
+
+ /**
+ * Fires a {@link SelectionEvent} to all the {@link SelectionListener
+ * SelectionListeners} currently added to the Grid in which this
+ * SelectionModel is.
+ * <p>
+ * Note that this is only a helper method, and routes the call all the
+ * way to Grid. A {@link SelectionModel} is not a
+ * {@link SelectionNotifier}
+ *
+ * @param oldSelection
+ * the complete {@link Collection} of the itemIds that were
+ * selected <em>before</em> this event happened
+ * @param newSelection
+ * the complete {@link Collection} of the itemIds that are
+ * selected <em>after</em> this event happened
+ */
+ protected void fireSelectionEvent(final Collection<Object> oldSelection,
+ final Collection<Object> newSelection) {
+ getParentGrid().fireSelectionEvent(oldSelection, newSelection);
+ }
+
+ @Override
+ public void generateData(Object itemId, Item item, JsonObject rowData) {
+ if (isSelected(itemId)) {
+ rowData.put(GridState.JSONKEY_SELECTED, true);
+ }
+ }
+
+ @Override
+ public void destroyData(Object itemId) {
+ // NO-OP
+ }
+
+ @Override
+ protected Object getItemId(String rowKey) {
+ return rowKey != null ? super.getItemId(rowKey) : null;
+ }
+ }
+
+ /**
+ * A default implementation of a {@link SelectionModel.Single}
+ */
+ public static class SingleSelectionModel extends AbstractSelectionModel
+ implements SelectionModel.Single {
+
+ @Override
+ protected void extend(AbstractClientConnector target) {
+ super.extend(target);
+ registerRpc(new SingleSelectionModelServerRpc() {
+
+ @Override
+ public void select(String rowKey) {
+ SingleSelectionModel.this.select(getItemId(rowKey), false);
+ }
+ });
+ }
+
+ @Override
+ public boolean select(final Object itemId) {
+ return select(itemId, true);
+ }
+
+ protected boolean select(final Object itemId, boolean refresh) {
+ if (itemId == null) {
+ return deselect(getSelectedRow());
+ }
+
+ checkItemIdExists(itemId);
+
+ final Object selectedRow = getSelectedRow();
+ final boolean modified = selection.add(itemId);
+ if (modified) {
+ final Collection<Object> deselected;
+ if (selectedRow != null) {
+ deselectInternal(selectedRow, false, true);
+ deselected = Collections.singleton(selectedRow);
+ } else {
+ deselected = Collections.emptySet();
+ }
+
+ fireSelectionEvent(deselected, selection);
+ }
+
+ if (refresh) {
+ refreshRow(itemId);
+ }
+
+ return modified;
+ }
+
+ private boolean deselect(final Object itemId) {
+ return deselectInternal(itemId, true, true);
+ }
+
+ private boolean deselectInternal(final Object itemId,
+ boolean fireEventIfNeeded, boolean refresh) {
+ final boolean modified = selection.remove(itemId);
+ if (modified) {
+ if (refresh) {
+ refreshRow(itemId);
+ }
+ if (fireEventIfNeeded) {
+ fireSelectionEvent(Collections.singleton(itemId),
+ Collections.emptySet());
+ }
+ }
+ return modified;
+ }
+
+ @Override
+ public Object getSelectedRow() {
+ if (selection.isEmpty()) {
+ return null;
+ } else {
+ return selection.iterator().next();
+ }
+ }
+
+ /**
+ * Resets the selection state.
+ * <p>
+ * If an item is selected, it will become deselected.
+ */
+ @Override
+ public void reset() {
+ deselect(getSelectedRow());
+ }
+
+ @Override
+ public void setDeselectAllowed(boolean deselectAllowed) {
+ getState().deselectAllowed = deselectAllowed;
+ }
+
+ @Override
+ public boolean isDeselectAllowed() {
+ return getState().deselectAllowed;
+ }
+
+ @Override
+ protected SingleSelectionModelState getState() {
+ return (SingleSelectionModelState) super.getState();
+ }
+ }
+
+ /**
+ * A default implementation for a {@link SelectionModel.None}
+ */
+ public static class NoSelectionModel extends AbstractSelectionModel
+ implements SelectionModel.None {
+
+ @Override
+ public boolean isSelected(final Object itemId) {
+ return false;
+ }
+
+ @Override
+ public Collection<Object> getSelectedRows() {
+ return Collections.emptyList();
+ }
+
+ /**
+ * Semantically resets the selection model.
+ * <p>
+ * Effectively a no-op.
+ */
+ @Override
+ public void reset() {
+ // NOOP
+ }
+ }
+
+ /**
+ * A default implementation of a {@link SelectionModel.Multi}
+ */
+ public static class MultiSelectionModel extends AbstractSelectionModel
+ implements SelectionModel.Multi {
+
+ /**
+ * The default selection size limit.
+ *
+ * @see #setSelectionLimit(int)
+ */
+ public static final int DEFAULT_MAX_SELECTIONS = 1000;
+
+ private int selectionLimit = DEFAULT_MAX_SELECTIONS;
+
+ @Override
+ protected void extend(AbstractClientConnector target) {
+ super.extend(target);
+ registerRpc(new MultiSelectionModelServerRpc() {
+
+ @Override
+ public void select(List<String> rowKeys) {
+ List<Object> items = new ArrayList<>();
+ for (String rowKey : rowKeys) {
+ items.add(getItemId(rowKey));
+ }
+ MultiSelectionModel.this.select(items, false);
+ }
+
+ @Override
+ public void deselect(List<String> rowKeys) {
+ List<Object> items = new ArrayList<>();
+ for (String rowKey : rowKeys) {
+ items.add(getItemId(rowKey));
+ }
+ MultiSelectionModel.this.deselect(items, false);
+ }
+
+ @Override
+ public void selectAll() {
+ MultiSelectionModel.this.selectAll(false);
+ }
+
+ @Override
+ public void deselectAll() {
+ MultiSelectionModel.this.deselectAll(false);
+ }
+ });
+ }
+
+ @Override
+ public boolean select(final Object... itemIds)
+ throws IllegalArgumentException {
+ if (itemIds != null) {
+ // select will fire the event
+ return select(Arrays.asList(itemIds));
+ } else {
+ throw new IllegalArgumentException(
+ "Vararg array of itemIds may not be null");
+ }
+ }
+
+ /**
+ * {@inheritDoc}
+ * <p>
+ * All items might not be selected if the limit set using
+ * {@link #setSelectionLimit(int)} is exceeded.
+ */
+ @Override
+ public boolean select(final Collection<?> itemIds)
+ throws IllegalArgumentException {
+ return select(itemIds, true);
+ }
+
+ protected boolean select(final Collection<?> itemIds, boolean refresh) {
+ if (itemIds == null) {
+ throw new IllegalArgumentException("itemIds may not be null");
+ }
+
+ // Sanity check
+ checkItemIdsExist(itemIds);
+
+ final boolean selectionWillChange = !selection.containsAll(itemIds)
+ && selection.size() < selectionLimit;
+ if (selectionWillChange) {
+ final HashSet<Object> oldSelection = new HashSet<>(selection);
+ if (selection.size() + itemIds.size() >= selectionLimit) {
+ // Add one at a time if there's a risk of overflow
+ Iterator<?> iterator = itemIds.iterator();
+ while (iterator.hasNext()
+ && selection.size() < selectionLimit) {
+ selection.add(iterator.next());
+ }
+ } else {
+ selection.addAll(itemIds);
+ }
+ fireSelectionEvent(oldSelection, selection);
+ }
+
+ updateAllSelectedState();
+
+ if (refresh) {
+ for (Object itemId : itemIds) {
+ refreshRow(itemId);
+ }
+ }
+
+ return selectionWillChange;
+ }
+
+ /**
+ * Sets the maximum number of rows that can be selected at once. This is
+ * a mechanism to prevent exhausting server memory in situations where
+ * users select lots of rows. If the limit is reached, newly selected
+ * rows will not become recorded.
+ * <p>
+ * Old selections are not discarded if the current number of selected
+ * row exceeds the new limit.
+ * <p>
+ * The default limit is {@value #DEFAULT_MAX_SELECTIONS} rows.
+ *
+ * @param selectionLimit
+ * the non-negative selection limit to set
+ * @throws IllegalArgumentException
+ * if the limit is negative
+ */
+ public void setSelectionLimit(int selectionLimit) {
+ if (selectionLimit < 0) {
+ throw new IllegalArgumentException(
+ "The selection limit must be non-negative");
+ }
+ this.selectionLimit = selectionLimit;
+ }
+
+ /**
+ * Gets the selection limit.
+ *
+ * @see #setSelectionLimit(int)
+ *
+ * @return the selection limit
+ */
+ public int getSelectionLimit() {
+ return selectionLimit;
+ }
+
+ @Override
+ public boolean deselect(final Object... itemIds)
+ throws IllegalArgumentException {
+ if (itemIds != null) {
+ // deselect will fire the event
+ return deselect(Arrays.asList(itemIds));
+ } else {
+ throw new IllegalArgumentException(
+ "Vararg array of itemIds may not be null");
+ }
+ }
+
+ @Override
+ public boolean deselect(final Collection<?> itemIds)
+ throws IllegalArgumentException {
+ return deselect(itemIds, true);
+ }
+
+ protected boolean deselect(final Collection<?> itemIds,
+ boolean refresh) {
+ if (itemIds == null) {
+ throw new IllegalArgumentException("itemIds may not be null");
+ }
+
+ final boolean hasCommonElements = !Collections.disjoint(itemIds,
+ selection);
+ if (hasCommonElements) {
+ final HashSet<Object> oldSelection = new HashSet<>(selection);
+ selection.removeAll(itemIds);
+ fireSelectionEvent(oldSelection, selection);
+ }
+
+ updateAllSelectedState();
+
+ if (refresh) {
+ for (Object itemId : itemIds) {
+ refreshRow(itemId);
+ }
+ }
+
+ return hasCommonElements;
+ }
+
+ @Override
+ public boolean selectAll() {
+ return selectAll(true);
+ }
+
+ protected boolean selectAll(boolean refresh) {
+ // select will fire the event
+ final Indexed container = getParentGrid().getContainerDataSource();
+ if (container != null) {
+ return select(container.getItemIds(), refresh);
+ } else if (selection.isEmpty()) {
+ return false;
+ } else {
+ /*
+ * this should never happen (no container but has a selection),
+ * but I guess the only theoretically correct course of
+ * action...
+ */
+ return deselectAll(false);
+ }
+ }
+
+ @Override
+ public boolean deselectAll() {
+ return deselectAll(true);
+ }
+
+ protected boolean deselectAll(boolean refresh) {
+ // deselect will fire the event
+ return deselect(getSelectedRows(), refresh);
+ }
+
+ /**
+ * {@inheritDoc}
+ * <p>
+ * The returned Collection is in <strong>order of selection</strong>
+ * &ndash; the item that was first selected will be first in the
+ * collection, and so on. Should an item have been selected twice
+ * without being deselected in between, it will have remained in its
+ * original position.
+ */
+ @Override
+ public Collection<Object> getSelectedRows() {
+ // overridden only for JavaDoc
+ return super.getSelectedRows();
+ }
+
+ /**
+ * Resets the selection model.
+ * <p>
+ * Equivalent to calling {@link #deselectAll()}
+ */
+ @Override
+ public void reset() {
+ deselectAll();
+ }
+
+ @Override
+ public boolean setSelected(Collection<?> itemIds)
+ throws IllegalArgumentException {
+ if (itemIds == null) {
+ throw new IllegalArgumentException("itemIds may not be null");
+ }
+
+ checkItemIdsExist(itemIds);
+
+ boolean changed = false;
+ Set<Object> selectedRows = new HashSet<>(itemIds);
+ final Collection<Object> oldSelection = getSelectedRows();
+ Set<Object> added = getDifference(selectedRows, selection);
+ if (!added.isEmpty()) {
+ changed = true;
+ selection.addAll(added);
+ for (Object id : added) {
+ refreshRow(id);
+ }
+ }
+
+ Set<Object> removed = getDifference(selection, selectedRows);
+ if (!removed.isEmpty()) {
+ changed = true;
+ selection.removeAll(removed);
+ for (Object id : removed) {
+ refreshRow(id);
+ }
+ }
+
+ if (changed) {
+ fireSelectionEvent(oldSelection, selection);
+ }
+
+ updateAllSelectedState();
+
+ return changed;
+ }
+
+ /**
+ * Compares two sets and returns a set containing all values that are
+ * present in the first, but not in the second.
+ *
+ * @param set1
+ * first item set
+ * @param set2
+ * second item set
+ * @return all values from set1 which are not present in set2
+ */
+ private static Set<Object> getDifference(Set<Object> set1,
+ Set<Object> set2) {
+ Set<Object> diff = new HashSet<>(set1);
+ diff.removeAll(set2);
+ return diff;
+ }
+
+ @Override
+ public boolean setSelected(Object... itemIds)
+ throws IllegalArgumentException {
+ if (itemIds != null) {
+ return setSelected(Arrays.asList(itemIds));
+ } else {
+ throw new IllegalArgumentException(
+ "Vararg array of itemIds may not be null");
+ }
+ }
+
+ private void updateAllSelectedState() {
+ int totalRowCount = getParentGrid().datasource.size();
+ int rows = Math.min(totalRowCount, selectionLimit);
+ if (getState().allSelected != selection.size() >= rows) {
+ getState().allSelected = selection.size() >= rows;
+ }
+ }
+
+ @Override
+ protected MultiSelectionModelState getState() {
+ return (MultiSelectionModelState) super.getState();
+ }
+ }
+
+ /**
+ * A data class which contains information which identifies a row in a
+ * {@link LegacyGrid}.
+ * <p>
+ * Since this class follows the <code>Flyweight</code>-pattern any instance
+ * of this object is subject to change without the user knowing it and so
+ * should not be stored anywhere outside of the method providing these
+ * instances.
+ */
+ public static class RowReference implements Serializable {
+ private final LegacyGrid grid;
+
+ private Object itemId;
+
+ /**
+ * Creates a new row reference for the given grid.
+ *
+ * @param grid
+ * the grid that the row belongs to
+ */
+ public RowReference(LegacyGrid grid) {
+ this.grid = grid;
+ }
+
+ /**
+ * Sets the identifying information for this row
+ *
+ * @param itemId
+ * the item id of the row
+ */
+ public void set(Object itemId) {
+ this.itemId = itemId;
+ }
+
+ /**
+ * Gets the grid that contains the referenced row.
+ *
+ * @return the grid that contains referenced row
+ */
+ public LegacyGrid getGrid() {
+ return grid;
+ }
+
+ /**
+ * Gets the item id of the row.
+ *
+ * @return the item id of the row
+ */
+ public Object getItemId() {
+ return itemId;
+ }
+
+ /**
+ * Gets the item for the row.
+ *
+ * @return the item for the row
+ */
+ public Item getItem() {
+ return grid.getContainerDataSource().getItem(itemId);
+ }
+ }
+
+ /**
+ * A data class which contains information which identifies a cell in a
+ * {@link LegacyGrid}.
+ * <p>
+ * Since this class follows the <code>Flyweight</code>-pattern any instance
+ * of this object is subject to change without the user knowing it and so
+ * should not be stored anywhere outside of the method providing these
+ * instances.
+ */
+ public static class CellReference implements Serializable {
+ private final RowReference rowReference;
+
+ private Object propertyId;
+
+ public CellReference(RowReference rowReference) {
+ this.rowReference = rowReference;
+ }
+
+ /**
+ * Sets the identifying information for this cell
+ *
+ * @param propertyId
+ * the property id of the column
+ */
+ public void set(Object propertyId) {
+ this.propertyId = propertyId;
+ }
+
+ /**
+ * Gets the grid that contains the referenced cell.
+ *
+ * @return the grid that contains referenced cell
+ */
+ public LegacyGrid getGrid() {
+ return rowReference.getGrid();
+ }
+
+ /**
+ * @return the property id of the column
+ */
+ public Object getPropertyId() {
+ return propertyId;
+ }
+
+ /**
+ * @return the property for the cell
+ */
+ public Property<?> getProperty() {
+ return getItem().getItemProperty(propertyId);
+ }
+
+ /**
+ * Gets the item id of the row of the cell.
+ *
+ * @return the item id of the row
+ */
+ public Object getItemId() {
+ return rowReference.getItemId();
+ }
+
+ /**
+ * Gets the item for the row of the cell.
+ *
+ * @return the item for the row
+ */
+ public Item getItem() {
+ return rowReference.getItem();
+ }
+
+ /**
+ * Gets the value of the cell.
+ *
+ * @return the value of the cell
+ */
+ public Object getValue() {
+ return getProperty().getValue();
+ }
+ }
+
+ /**
+ * A callback interface for generating custom style names for Grid rows.
+ *
+ * @see LegacyGrid#setRowStyleGenerator(RowStyleGenerator)
+ */
+ public interface RowStyleGenerator extends Serializable {
+
+ /**
+ * Called by Grid to generate a style name for a row.
+ *
+ * @param row
+ * the row to generate a style for
+ * @return the style name to add to this row, or {@code null} to not set
+ * any style
+ */
+ public String getStyle(RowReference row);
+ }
+
+ /**
+ * A callback interface for generating custom style names for Grid cells.
+ *
+ * @see LegacyGrid#setCellStyleGenerator(CellStyleGenerator)
+ */
+ public interface CellStyleGenerator extends Serializable {
+
+ /**
+ * Called by Grid to generate a style name for a column.
+ *
+ * @param cell
+ * the cell to generate a style for
+ * @return the style name to add to this cell, or {@code null} to not
+ * set any style
+ */
+ public String getStyle(CellReference cell);
+ }
+
+ /**
+ * A callback interface for generating optional descriptions (tooltips) for
+ * Grid rows. If a description is generated for a row, it is used for all
+ * the cells in the row for which a {@link CellDescriptionGenerator cell
+ * description} is not generated.
+ *
+ * @see LegacyGrid#setRowDescriptionGenerator
+ *
+ * @since 7.6
+ */
+ public interface RowDescriptionGenerator extends Serializable {
+
+ /**
+ * Called by Grid to generate a description (tooltip) for a row. The
+ * description may contain HTML which is rendered directly; if this is
+ * not desired the returned string must be escaped by the implementing
+ * method.
+ *
+ * @param row
+ * the row to generate a description for
+ * @return the row description or {@code null} for no description
+ */
+ public String getDescription(RowReference row);
+ }
+
+ /**
+ * A callback interface for generating optional descriptions (tooltips) for
+ * Grid cells. If a cell has both a {@link RowDescriptionGenerator row
+ * description} and a cell description, the latter has precedence.
+ *
+ * @see LegacyGrid#setCellDescriptionGenerator(CellDescriptionGenerator)
+ *
+ * @since 7.6
+ */
+ public interface CellDescriptionGenerator extends Serializable {
+
+ /**
+ * Called by Grid to generate a description (tooltip) for a cell. The
+ * description may contain HTML which is rendered directly; if this is
+ * not desired the returned string must be escaped by the implementing
+ * method.
+ *
+ * @param cell
+ * the cell to generate a description for
+ * @return the cell description or {@code null} for no description
+ */
+ public String getDescription(CellReference cell);
+ }
+
+ /**
+ * Class for generating all row and cell related data for the essential
+ * parts of Grid.
+ */
+ private class RowDataGenerator implements DataGenerator {
+
+ private void put(String key, String value, JsonObject object) {
+ if (value != null && !value.isEmpty()) {
+ object.put(key, value);
+ }
+ }
+
+ @Override
+ public void generateData(Object itemId, Item item, JsonObject rowData) {
+ RowReference row = new RowReference(LegacyGrid.this);
+ row.set(itemId);
+
+ if (rowStyleGenerator != null) {
+ String style = rowStyleGenerator.getStyle(row);
+ put(GridState.JSONKEY_ROWSTYLE, style, rowData);
+ }
+
+ if (rowDescriptionGenerator != null) {
+ String description = rowDescriptionGenerator
+ .getDescription(row);
+ put(GridState.JSONKEY_ROWDESCRIPTION, description, rowData);
+
+ }
+
+ JsonObject cellStyles = Json.createObject();
+ JsonObject cellData = Json.createObject();
+ JsonObject cellDescriptions = Json.createObject();
+
+ CellReference cell = new CellReference(row);
+
+ for (Column column : getColumns()) {
+ cell.set(column.getPropertyId());
+
+ writeData(cell, cellData);
+ writeStyles(cell, cellStyles);
+ writeDescriptions(cell, cellDescriptions);
+ }
+
+ if (cellDescriptionGenerator != null
+ && cellDescriptions.keys().length > 0) {
+ rowData.put(GridState.JSONKEY_CELLDESCRIPTION,
+ cellDescriptions);
+ }
+
+ if (cellStyleGenerator != null && cellStyles.keys().length > 0) {
+ rowData.put(GridState.JSONKEY_CELLSTYLES, cellStyles);
+ }
+
+ rowData.put(GridState.JSONKEY_DATA, cellData);
+ }
+
+ private void writeStyles(CellReference cell, JsonObject styles) {
+ if (cellStyleGenerator != null) {
+ String style = cellStyleGenerator.getStyle(cell);
+ put(columnKeys.key(cell.getPropertyId()), style, styles);
+ }
+ }
+
+ private void writeDescriptions(CellReference cell,
+ JsonObject descriptions) {
+ if (cellDescriptionGenerator != null) {
+ String description = cellDescriptionGenerator
+ .getDescription(cell);
+ put(columnKeys.key(cell.getPropertyId()), description,
+ descriptions);
+ }
+ }
+
+ private void writeData(CellReference cell, JsonObject data) {
+ Column column = getColumn(cell.getPropertyId());
+ LegacyConverter<?, ?> converter = column.getConverter();
+ Renderer<?> renderer = column.getRenderer();
+
+ Item item = cell.getItem();
+ Object modelValue = item.getItemProperty(cell.getPropertyId())
+ .getValue();
+
+ data.put(columnKeys.key(cell.getPropertyId()), AbstractRenderer
+ .encodeValue(modelValue, renderer, converter, getLocale()));
+ }
+
+ @Override
+ public void destroyData(Object itemId) {
+ // NO-OP
+ }
+ }
+
+ /**
+ * Abstract base class for Grid header and footer sections.
+ *
+ * @since 7.6
+ * @param <ROWTYPE>
+ * the type of the rows in the section
+ */
+ public abstract static class StaticSection<ROWTYPE extends StaticSection.StaticRow<?>>
+ implements Serializable {
+
+ /**
+ * Abstract base class for Grid header and footer rows.
+ *
+ * @param <CELLTYPE>
+ * the type of the cells in the row
+ */
+ public abstract static class StaticRow<CELLTYPE extends StaticCell>
+ implements Serializable {
+
+ private RowState rowState = new RowState();
+ protected StaticSection<?> section;
+ private Map<Object, CELLTYPE> cells = new LinkedHashMap<>();
+ private Map<Set<CELLTYPE>, CELLTYPE> cellGroups = new HashMap<>();
+
+ protected StaticRow(StaticSection<?> section) {
+ this.section = section;
+ }
+
+ protected void addCell(Object propertyId) {
+ CELLTYPE cell = createCell();
+ cell.setColumnId(
+ section.grid.getColumn(propertyId).getState().id);
+ cells.put(propertyId, cell);
+ rowState.cells.add(cell.getCellState());
+ }
+
+ protected void removeCell(Object propertyId) {
+ CELLTYPE cell = cells.remove(propertyId);
+ if (cell != null) {
+ Set<CELLTYPE> cellGroupForCell = getCellGroupForCell(cell);
+ if (cellGroupForCell != null) {
+ removeCellFromGroup(cell, cellGroupForCell);
+ }
+ rowState.cells.remove(cell.getCellState());
+ }
+ }
+
+ private void removeCellFromGroup(CELLTYPE cell,
+ Set<CELLTYPE> cellGroup) {
+ String columnId = cell.getColumnId();
+ for (Set<String> group : rowState.cellGroups.keySet()) {
+ if (group.contains(columnId)) {
+ if (group.size() > 2) {
+ // Update map key correctly
+ CELLTYPE mergedCell = cellGroups.remove(cellGroup);
+ cellGroup.remove(cell);
+ cellGroups.put(cellGroup, mergedCell);
+
+ group.remove(columnId);
+ } else {
+ rowState.cellGroups.remove(group);
+ cellGroups.remove(cellGroup);
+ }
+ return;
+ }
+ }
+ }
+
+ /**
+ * Creates and returns a new instance of the cell type.
+ *
+ * @return the created cell
+ */
+ protected abstract CELLTYPE createCell();
+
+ protected RowState getRowState() {
+ return rowState;
+ }
+
+ /**
+ * Returns the cell for the given property id on this row. If the
+ * column is merged returned cell is the cell for the whole group.
+ *
+ * @param propertyId
+ * the property id of the column
+ * @return the cell for the given property, merged cell for merged
+ * properties, null if not found
+ */
+ public CELLTYPE getCell(Object propertyId) {
+ CELLTYPE cell = cells.get(propertyId);
+ Set<CELLTYPE> cellGroup = getCellGroupForCell(cell);
+ if (cellGroup != null) {
+ cell = cellGroups.get(cellGroup);
+ }
+ return cell;
+ }
+
+ /**
+ * Merges columns cells in a row
+ *
+ * @param propertyIds
+ * The property ids of columns to merge
+ * @return The remaining visible cell after the merge
+ */
+ public CELLTYPE join(Object... propertyIds) {
+ assert propertyIds.length > 1 : "You need to merge at least 2 properties";
+
+ Set<CELLTYPE> cells = new HashSet<>();
+ for (int i = 0; i < propertyIds.length; ++i) {
+ cells.add(getCell(propertyIds[i]));
+ }
+
+ return join(cells);
+ }
+
+ /**
+ * Merges columns cells in a row
+ *
+ * @param cells
+ * The cells to merge. Must be from the same row.
+ * @return The remaining visible cell after the merge
+ */
+ public CELLTYPE join(CELLTYPE... cells) {
+ assert cells.length > 1 : "You need to merge at least 2 cells";
+
+ return join(new HashSet<>(Arrays.asList(cells)));
+ }
+
+ protected CELLTYPE join(Set<CELLTYPE> cells) {
+ for (CELLTYPE cell : cells) {
+ if (getCellGroupForCell(cell) != null) {
+ throw new IllegalArgumentException(
+ "Cell already merged");
+ } else if (!this.cells.containsValue(cell)) {
+ throw new IllegalArgumentException(
+ "Cell does not exist on this row");
+ }
+ }
+
+ // Create new cell data for the group
+ CELLTYPE newCell = createCell();
+
+ Set<String> columnGroup = new HashSet<>();
+ for (CELLTYPE cell : cells) {
+ columnGroup.add(cell.getColumnId());
+ }
+ rowState.cellGroups.put(columnGroup, newCell.getCellState());
+ cellGroups.put(cells, newCell);
+ return newCell;
+ }
+
+ private Set<CELLTYPE> getCellGroupForCell(CELLTYPE cell) {
+ for (Set<CELLTYPE> group : cellGroups.keySet()) {
+ if (group.contains(cell)) {
+ return group;
+ }
+ }
+ return null;
+ }
+
+ /**
+ * Returns the custom style name for this row.
+ *
+ * @return the style name or null if no style name has been set
+ */
+ public String getStyleName() {
+ return getRowState().styleName;
+ }
+
+ /**
+ * Sets a custom style name for this row.
+ *
+ * @param styleName
+ * the style name to set or null to not use any style
+ * name
+ */
+ public void setStyleName(String styleName) {
+ getRowState().styleName = styleName;
+ }
+
+ /**
+ * Writes the declarative design to the given table row element.
+ *
+ * @since 7.5.0
+ * @param trElement
+ * Element to write design to
+ * @param designContext
+ * the design context
+ */
+ protected void writeDesign(Element trElement,
+ DesignContext designContext) {
+ Set<CELLTYPE> visited = new HashSet<>();
+ for (LegacyGrid.Column column : section.grid.getColumns()) {
+ CELLTYPE cell = getCell(column.getPropertyId());
+ if (visited.contains(cell)) {
+ continue;
+ }
+ visited.add(cell);
+
+ Element cellElement = trElement
+ .appendElement(getCellTagName());
+ cell.writeDesign(cellElement, designContext);
+
+ for (Entry<Set<CELLTYPE>, CELLTYPE> entry : cellGroups
+ .entrySet()) {
+ if (entry.getValue() == cell) {
+ cellElement.attr("colspan",
+ "" + entry.getKey().size());
+ break;
+ }
+ }
+ }
+ }
+
+ /**
+ * Reads the declarative design from the given table row element.
+ *
+ * @since 7.5.0
+ * @param trElement
+ * Element to read design from
+ * @param designContext
+ * the design context
+ * @throws DesignException
+ * if the given table row contains unexpected children
+ */
+ protected void readDesign(Element trElement,
+ DesignContext designContext) throws DesignException {
+ Elements cellElements = trElement.children();
+ int totalColSpans = 0;
+ for (int i = 0; i < cellElements.size(); ++i) {
+ Element element = cellElements.get(i);
+ if (!element.tagName().equals(getCellTagName())) {
+ throw new DesignException(
+ "Unexpected element in tr while expecting "
+ + getCellTagName() + ": "
+ + element.tagName());
+ }
+
+ int columnIndex = i + totalColSpans;
+
+ int colspan = DesignAttributeHandler.readAttribute(
+ "colspan", element.attributes(), 1, int.class);
+
+ Set<CELLTYPE> cells = new HashSet<>();
+ for (int c = 0; c < colspan; ++c) {
+ cells.add(getCell(section.grid.getColumns()
+ .get(columnIndex + c).getPropertyId()));
+ }
+
+ if (colspan > 1) {
+ totalColSpans += colspan - 1;
+ join(cells).readDesign(element, designContext);
+ } else {
+ cells.iterator().next().readDesign(element,
+ designContext);
+ }
+ }
+ }
+
+ abstract protected String getCellTagName();
+
+ void detach() {
+ for (CELLTYPE cell : cells.values()) {
+ cell.detach();
+ }
+ }
+ }
+
+ /**
+ * A header or footer cell. Has a simple textual caption.
+ */
+ abstract static class StaticCell implements Serializable {
+
+ private CellState cellState = new CellState();
+ private StaticRow<?> row;
+
+ protected StaticCell(StaticRow<?> row) {
+ this.row = row;
+ }
+
+ void setColumnId(String id) {
+ cellState.columnId = id;
+ }
+
+ String getColumnId() {
+ return cellState.columnId;
+ }
+
+ /**
+ * Gets the row where this cell is.
+ *
+ * @return row for this cell
+ */
+ public StaticRow<?> getRow() {
+ return row;
+ }
+
+ protected CellState getCellState() {
+ return cellState;
+ }
+
+ /**
+ * Sets the text displayed in this cell.
+ *
+ * @param text
+ * a plain text caption
+ */
+ public void setText(String text) {
+ removeComponentIfPresent();
+ cellState.text = text;
+ cellState.type = GridStaticCellType.TEXT;
+ row.section.markAsDirty();
+ }
+
+ /**
+ * Returns the text displayed in this cell.
+ *
+ * @return the plain text caption
+ */
+ public String getText() {
+ if (cellState.type != GridStaticCellType.TEXT) {
+ throw new IllegalStateException(
+ "Cannot fetch Text from a cell with type "
+ + cellState.type);
+ }
+ return cellState.text;
+ }
+
+ /**
+ * Returns the HTML content displayed in this cell.
+ *
+ * @return the html
+ *
+ */
+ public String getHtml() {
+ if (cellState.type != GridStaticCellType.HTML) {
+ throw new IllegalStateException(
+ "Cannot fetch HTML from a cell with type "
+ + cellState.type);
+ }
+ return cellState.html;
+ }
+
+ /**
+ * Sets the HTML content displayed in this cell.
+ *
+ * @param html
+ * the html to set
+ */
+ public void setHtml(String html) {
+ removeComponentIfPresent();
+ cellState.html = html;
+ cellState.type = GridStaticCellType.HTML;
+ row.section.markAsDirty();
+ }
+
+ /**
+ * Returns the component displayed in this cell.
+ *
+ * @return the component
+ */
+ public Component getComponent() {
+ if (cellState.type != GridStaticCellType.WIDGET) {
+ throw new IllegalStateException(
+ "Cannot fetch Component from a cell with type "
+ + cellState.type);
+ }
+ return (Component) cellState.connector;
+ }
+
+ /**
+ * Sets the component displayed in this cell.
+ *
+ * @param component
+ * the component to set
+ */
+ public void setComponent(Component component) {
+ removeComponentIfPresent();
+ component.setParent(row.section.grid);
+ cellState.connector = component;
+ cellState.type = GridStaticCellType.WIDGET;
+ row.section.markAsDirty();
+ }
+
+ /**
+ * Returns the type of content stored in this cell.
+ *
+ * @return cell content type
+ */
+ public GridStaticCellType getCellType() {
+ return cellState.type;
+ }
+
+ /**
+ * Returns the custom style name for this cell.
+ *
+ * @return the style name or null if no style name has been set
+ */
+ public String getStyleName() {
+ return cellState.styleName;
+ }
+
+ /**
+ * Sets a custom style name for this cell.
+ *
+ * @param styleName
+ * the style name to set or null to not use any style
+ * name
+ */
+ public void setStyleName(String styleName) {
+ cellState.styleName = styleName;
+ row.section.markAsDirty();
+ }
+
+ private void removeComponentIfPresent() {
+ Component component = (Component) cellState.connector;
+ if (component != null) {
+ component.setParent(null);
+ cellState.connector = null;
+ }
+ }
+
+ /**
+ * Writes the declarative design to the given table cell element.
+ *
+ * @since 7.5.0
+ * @param cellElement
+ * Element to write design to
+ * @param designContext
+ * the design context
+ */
+ protected void writeDesign(Element cellElement,
+ DesignContext designContext) {
+ switch (cellState.type) {
+ case TEXT:
+ cellElement.attr("plain-text", true);
+ cellElement.appendText(getText());
+ break;
+ case HTML:
+ cellElement.append(getHtml());
+ break;
+ case WIDGET:
+ cellElement.appendChild(
+ designContext.createElement(getComponent()));
+ break;
+ }
+ }
+
+ /**
+ * Reads the declarative design from the given table cell element.
+ *
+ * @since 7.5.0
+ * @param cellElement
+ * Element to read design from
+ * @param designContext
+ * the design context
+ */
+ protected void readDesign(Element cellElement,
+ DesignContext designContext) {
+ if (!cellElement.hasAttr("plain-text")) {
+ if (cellElement.children().size() > 0
+ && cellElement.child(0).tagName().contains("-")) {
+ setComponent(
+ designContext.readDesign(cellElement.child(0)));
+ } else {
+ setHtml(cellElement.html());
+ }
+ } else {
+ // text – need to unescape HTML entities
+ setText(DesignFormatter
+ .decodeFromTextNode(cellElement.html()));
+ }
+ }
+
+ void detach() {
+ removeComponentIfPresent();
+ }
+ }
+
+ protected LegacyGrid grid;
+ protected List<ROWTYPE> rows = new ArrayList<>();
+
+ /**
+ * Sets the visibility of the whole section.
+ *
+ * @param visible
+ * true to show this section, false to hide
+ */
+ public void setVisible(boolean visible) {
+ if (getSectionState().visible != visible) {
+ getSectionState().visible = visible;
+ markAsDirty();
+ }
+ }
+
+ /**
+ * Returns the visibility of this section.
+ *
+ * @return true if visible, false otherwise.
+ */
+ public boolean isVisible() {
+ return getSectionState().visible;
+ }
+
+ /**
+ * Removes the row at the given position.
+ *
+ * @param rowIndex
+ * the position of the row
+ *
+ * @throws IllegalArgumentException
+ * if no row exists at given index
+ * @see #removeRow(StaticRow)
+ * @see #addRowAt(int)
+ * @see #appendRow()
+ * @see #prependRow()
+ */
+ public ROWTYPE removeRow(int rowIndex) {
+ if (rowIndex >= rows.size() || rowIndex < 0) {
+ throw new IllegalArgumentException(
+ "No row at given index " + rowIndex);
+ }
+ ROWTYPE row = rows.remove(rowIndex);
+ row.detach();
+ getSectionState().rows.remove(rowIndex);
+
+ markAsDirty();
+ return row;
+ }
+
+ /**
+ * Removes the given row from the section.
+ *
+ * @param row
+ * the row to be removed
+ *
+ * @throws IllegalArgumentException
+ * if the row does not exist in this section
+ * @see #removeRow(int)
+ * @see #addRowAt(int)
+ * @see #appendRow()
+ * @see #prependRow()
+ */
+ public void removeRow(ROWTYPE row) {
+ try {
+ removeRow(rows.indexOf(row));
+ } catch (IndexOutOfBoundsException e) {
+ throw new IllegalArgumentException(
+ "Section does not contain the given row");
+ }
+ }
+
+ /**
+ * Gets row at given index.
+ *
+ * @param rowIndex
+ * 0 based index for row. Counted from top to bottom
+ * @return row at given index
+ */
+ public ROWTYPE getRow(int rowIndex) {
+ if (rowIndex >= rows.size() || rowIndex < 0) {
+ throw new IllegalArgumentException(
+ "No row at given index " + rowIndex);
+ }
+ return rows.get(rowIndex);
+ }
+
+ /**
+ * Adds a new row at the top of this section.
+ *
+ * @return the new row
+ * @see #appendRow()
+ * @see #addRowAt(int)
+ * @see #removeRow(StaticRow)
+ * @see #removeRow(int)
+ */
+ public ROWTYPE prependRow() {
+ return addRowAt(0);
+ }
+
+ /**
+ * Adds a new row at the bottom of this section.
+ *
+ * @return the new row
+ * @see #prependRow()
+ * @see #addRowAt(int)
+ * @see #removeRow(StaticRow)
+ * @see #removeRow(int)
+ */
+ public ROWTYPE appendRow() {
+ return addRowAt(rows.size());
+ }
+
+ /**
+ * Inserts a new row at the given position.
+ *
+ * @param index
+ * the position at which to insert the row
+ * @return the new row
+ *
+ * @throws IndexOutOfBoundsException
+ * if the index is out of bounds
+ * @see #appendRow()
+ * @see #prependRow()
+ * @see #removeRow(StaticRow)
+ * @see #removeRow(int)
+ */
+ public ROWTYPE addRowAt(int index) {
+ if (index > rows.size() || index < 0) {
+ throw new IllegalArgumentException(
+ "Unable to add row at index " + index);
+ }
+ ROWTYPE row = createRow();
+ rows.add(index, row);
+ getSectionState().rows.add(index, row.getRowState());
+
+ for (Object id : grid.columns.keySet()) {
+ row.addCell(id);
+ }
+
+ markAsDirty();
+ return row;
+ }
+
+ /**
+ * Gets the amount of rows in this section.
+ *
+ * @return row count
+ */
+ public int getRowCount() {
+ return rows.size();
+ }
+
+ protected abstract GridStaticSectionState getSectionState();
+
+ protected abstract ROWTYPE createRow();
+
+ /**
+ * Informs the grid that state has changed and it should be redrawn.
+ */
+ protected void markAsDirty() {
+ grid.markAsDirty();
+ }
+
+ /**
+ * Removes a column for given property id from the section.
+ *
+ * @param propertyId
+ * property to be removed
+ */
+ protected void removeColumn(Object propertyId) {
+ for (ROWTYPE row : rows) {
+ row.removeCell(propertyId);
+ }
+ }
+
+ /**
+ * Adds a column for given property id to the section.
+ *
+ * @param propertyId
+ * property to be added
+ */
+ protected void addColumn(Object propertyId) {
+ for (ROWTYPE row : rows) {
+ row.addCell(propertyId);
+ }
+ }
+
+ /**
+ * Performs a sanity check that section is in correct state.
+ *
+ * @throws IllegalStateException
+ * if merged cells are not i n continuous range
+ */
+ protected void sanityCheck() throws IllegalStateException {
+ List<String> columnOrder = grid.getState().columnOrder;
+ for (ROWTYPE row : rows) {
+ for (Set<String> cellGroup : row.getRowState().cellGroups
+ .keySet()) {
+ if (!checkCellGroupAndOrder(columnOrder, cellGroup)) {
+ throw new IllegalStateException(
+ "Not all merged cells were in a continuous range.");
+ }
+ }
+ }
+ }
+
+ private boolean checkCellGroupAndOrder(List<String> columnOrder,
+ Set<String> cellGroup) {
+ if (!columnOrder.containsAll(cellGroup)) {
+ return false;
+ }
+
+ for (int i = 0; i < columnOrder.size(); ++i) {
+ if (!cellGroup.contains(columnOrder.get(i))) {
+ continue;
+ }
+
+ for (int j = 1; j < cellGroup.size(); ++j) {
+ if (!cellGroup.contains(columnOrder.get(i + j))) {
+ return false;
+ }
+ }
+ return true;
+ }
+ return false;
+ }
+
+ /**
+ * Writes the declarative design to the given table section element.
+ *
+ * @since 7.5.0
+ * @param tableSectionElement
+ * Element to write design to
+ * @param designContext
+ * the design context
+ */
+ protected void writeDesign(Element tableSectionElement,
+ DesignContext designContext) {
+ for (ROWTYPE row : rows) {
+ row.writeDesign(tableSectionElement.appendElement("tr"),
+ designContext);
+ }
+ }
+
+ /**
+ * Writes the declarative design from the given table section element.
+ *
+ * @since 7.5.0
+ * @param tableSectionElement
+ * Element to read design from
+ * @param designContext
+ * the design context
+ * @throws DesignException
+ * if the table section contains unexpected children
+ */
+ protected void readDesign(Element tableSectionElement,
+ DesignContext designContext) throws DesignException {
+ while (rows.size() > 0) {
+ removeRow(0);
+ }
+
+ for (Element row : tableSectionElement.children()) {
+ if (!row.tagName().equals("tr")) {
+ throw new DesignException("Unexpected element in "
+ + tableSectionElement.tagName() + ": "
+ + row.tagName());
+ }
+ appendRow().readDesign(row, designContext);
+ }
+ }
+ }
+
+ /**
+ * Represents the header section of a Grid.
+ */
+ protected static class Header extends StaticSection<HeaderRow> {
+
+ private HeaderRow defaultRow = null;
+ private final GridStaticSectionState headerState = new GridStaticSectionState();
+
+ protected Header(LegacyGrid grid) {
+ this.grid = grid;
+ grid.getState(true).header = headerState;
+ HeaderRow row = createRow();
+ rows.add(row);
+ setDefaultRow(row);
+ getSectionState().rows.add(row.getRowState());
+ }
+
+ /**
+ * Sets the default row of this header. The default row is a special
+ * header row providing a user interface for sorting columns.
+ *
+ * @param row
+ * the new default row, or null for no default row
+ *
+ * @throws IllegalArgumentException
+ * this header does not contain the row
+ */
+ public void setDefaultRow(HeaderRow row) {
+ if (row == defaultRow) {
+ return;
+ }
+
+ if (row != null && !rows.contains(row)) {
+ throw new IllegalArgumentException(
+ "Cannot set a default row that does not exist in the section");
+ }
+
+ if (defaultRow != null) {
+ defaultRow.setDefaultRow(false);
+ }
+
+ if (row != null) {
+ row.setDefaultRow(true);
+ }
+
+ defaultRow = row;
+ markAsDirty();
+ }
+
+ /**
+ * Returns the current default row of this header. The default row is a
+ * special header row providing a user interface for sorting columns.
+ *
+ * @return the default row or null if no default row set
+ */
+ public HeaderRow getDefaultRow() {
+ return defaultRow;
+ }
+
+ @Override
+ protected GridStaticSectionState getSectionState() {
+ return headerState;
+ }
+
+ @Override
+ protected HeaderRow createRow() {
+ return new HeaderRow(this);
+ }
+
+ @Override
+ public HeaderRow removeRow(int rowIndex) {
+ HeaderRow row = super.removeRow(rowIndex);
+ if (row == defaultRow) {
+ // Default Header Row was just removed.
+ setDefaultRow(null);
+ }
+ return row;
+ }
+
+ @Override
+ protected void sanityCheck() throws IllegalStateException {
+ super.sanityCheck();
+
+ boolean hasDefaultRow = false;
+ for (HeaderRow row : rows) {
+ if (row.getRowState().defaultRow) {
+ if (!hasDefaultRow) {
+ hasDefaultRow = true;
+ } else {
+ throw new IllegalStateException(
+ "Multiple default rows in header");
+ }
+ }
+ }
+ }
+
+ @Override
+ protected void readDesign(Element tableSectionElement,
+ DesignContext designContext) {
+ super.readDesign(tableSectionElement, designContext);
+
+ if (defaultRow == null && !rows.isEmpty()) {
+ grid.setDefaultHeaderRow(rows.get(0));
+ }
+ }
+ }
+
+ /**
+ * Represents a header row in Grid.
+ */
+ public static class HeaderRow extends StaticSection.StaticRow<HeaderCell> {
+
+ protected HeaderRow(StaticSection<?> section) {
+ super(section);
+ }
+
+ private void setDefaultRow(boolean value) {
+ getRowState().defaultRow = value;
+ }
+
+ private boolean isDefaultRow() {
+ return getRowState().defaultRow;
+ }
+
+ @Override
+ protected HeaderCell createCell() {
+ return new HeaderCell(this);
+ }
+
+ @Override
+ protected String getCellTagName() {
+ return "th";
+ }
+
+ @Override
+ protected void writeDesign(Element trElement,
+ DesignContext designContext) {
+ super.writeDesign(trElement, designContext);
+
+ if (section.grid.getDefaultHeaderRow() == this) {
+ DesignAttributeHandler.writeAttribute("default",
+ trElement.attributes(), true, null, boolean.class);
+ }
+ }
+
+ @Override
+ protected void readDesign(Element trElement,
+ DesignContext designContext) {
+ super.readDesign(trElement, designContext);
+
+ boolean defaultRow = DesignAttributeHandler.readAttribute("default",
+ trElement.attributes(), false, boolean.class);
+ if (defaultRow) {
+ section.grid.setDefaultHeaderRow(this);
+ }
+ }
+ }
+
+ /**
+ * Represents a header cell in Grid. Can be a merged cell for multiple
+ * columns.
+ */
+ public static class HeaderCell extends StaticSection.StaticCell {
+
+ protected HeaderCell(HeaderRow row) {
+ super(row);
+ }
+ }
+
+ /**
+ * Represents the footer section of a Grid. By default Footer is not
+ * visible.
+ */
+ protected static class Footer extends StaticSection<FooterRow> {
+
+ private final GridStaticSectionState footerState = new GridStaticSectionState();
+
+ protected Footer(LegacyGrid grid) {
+ this.grid = grid;
+ grid.getState(true).footer = footerState;
+ }
+
+ @Override
+ protected GridStaticSectionState getSectionState() {
+ return footerState;
+ }
+
+ @Override
+ protected FooterRow createRow() {
+ return new FooterRow(this);
+ }
+
+ @Override
+ protected void sanityCheck() throws IllegalStateException {
+ super.sanityCheck();
+ }
+ }
+
+ /**
+ * Represents a footer row in Grid.
+ */
+ public static class FooterRow extends StaticSection.StaticRow<FooterCell> {
+
+ protected FooterRow(StaticSection<?> section) {
+ super(section);
+ }
+
+ @Override
+ protected FooterCell createCell() {
+ return new FooterCell(this);
+ }
+
+ @Override
+ protected String getCellTagName() {
+ return "td";
+ }
+
+ }
+
+ /**
+ * Represents a footer cell in Grid.
+ */
+ public static class FooterCell extends StaticSection.StaticCell {
+
+ protected FooterCell(FooterRow row) {
+ super(row);
+ }
+ }
+
+ /**
+ * A column in the grid. Can be obtained by calling
+ * {@link LegacyGrid#getColumn(Object propertyId)}.
+ */
+ public static class Column implements Serializable {
+
+ /**
+ * The state of the column shared to the client
+ */
+ private final GridColumnState state;
+
+ /**
+ * The grid this column is associated with
+ */
+ private final LegacyGrid grid;
+
+ /**
+ * Backing property for column
+ */
+ private final Object propertyId;
+
+ private LegacyConverter<?, Object> converter;
+
+ /**
+ * A check for allowing the
+ * {@link #Column(LegacyGrid, GridColumnState, Object) constructor} to call
+ * {@link #setConverter(LegacyConverter)} with a <code>null</code>, even
+ * if model and renderer aren't compatible.
+ */
+ private boolean isFirstConverterAssignment = true;
+
+ /**
+ * Internally used constructor.
+ *
+ * @param grid
+ * The grid this column belongs to. Should not be null.
+ * @param state
+ * the shared state of this column
+ * @param propertyId
+ * the backing property id for this column
+ */
+ Column(LegacyGrid grid, GridColumnState state, Object propertyId) {
+ this.grid = grid;
+ this.state = state;
+ this.propertyId = propertyId;
+ internalSetRenderer(new TextRenderer());
+ }
+
+ /**
+ * Returns the serializable state of this column that is sent to the
+ * client side connector.
+ *
+ * @return the internal state of the column
+ */
+ GridColumnState getState() {
+ return state;
+ }
+
+ /**
+ * Returns the property id for the backing property of this Column
+ *
+ * @return property id
+ */
+ public Object getPropertyId() {
+ return propertyId;
+ }
+
+ /**
+ * Returns the caption of the header. By default the header caption is
+ * the property id of the column.
+ *
+ * @return the text in the default row of header.
+ *
+ * @throws IllegalStateException
+ * if the column no longer is attached to the grid
+ */
+ public String getHeaderCaption() throws IllegalStateException {
+ checkColumnIsAttached();
+
+ return state.headerCaption;
+ }
+
+ /**
+ * Sets the caption of the header. This caption is also used as the
+ * hiding toggle caption, unless it is explicitly set via
+ * {@link #setHidingToggleCaption(String)}.
+ *
+ * @param caption
+ * the text to show in the caption
+ * @return the column itself
+ *
+ * @throws IllegalStateException
+ * if the column is no longer attached to any grid
+ */
+ public Column setHeaderCaption(String caption)
+ throws IllegalStateException {
+ checkColumnIsAttached();
+ if (caption == null) {
+ caption = ""; // Render null as empty
+ }
+ state.headerCaption = caption;
+
+ HeaderRow row = grid.getHeader().getDefaultRow();
+ if (row != null) {
+ row.getCell(grid.getPropertyIdByColumnId(state.id))
+ .setText(caption);
+ }
+ return this;
+ }
+
+ /**
+ * Gets the caption of the hiding toggle for this column.
+ *
+ * @since 7.5.0
+ * @see #setHidingToggleCaption(String)
+ * @return the caption for the hiding toggle for this column
+ * @throws IllegalStateException
+ * if the column is no longer attached to any grid
+ */
+ public String getHidingToggleCaption() throws IllegalStateException {
+ checkColumnIsAttached();
+ return state.hidingToggleCaption;
+ }
+
+ /**
+ * Sets the caption of the hiding toggle for this column. Shown in the
+ * toggle for this column in the grid's sidebar when the column is
+ * {@link #isHidable() hidable}.
+ * <p>
+ * The default value is <code>null</code>, and in that case the column's
+ * {@link #getHeaderCaption() header caption} is used.
+ * <p>
+ * <em>NOTE:</em> setting this to empty string might cause the hiding
+ * toggle to not render correctly.
+ *
+ * @since 7.5.0
+ * @param hidingToggleCaption
+ * the text to show in the column hiding toggle
+ * @return the column itself
+ * @throws IllegalStateException
+ * if the column is no longer attached to any grid
+ */
+ public Column setHidingToggleCaption(String hidingToggleCaption)
+ throws IllegalStateException {
+ checkColumnIsAttached();
+ state.hidingToggleCaption = hidingToggleCaption;
+ grid.markAsDirty();
+ return this;
+ }
+
+ /**
+ * Returns the width (in pixels). By default a column is 100px wide.
+ *
+ * @return the width in pixels of the column
+ * @throws IllegalStateException
+ * if the column is no longer attached to any grid
+ */
+ public double getWidth() throws IllegalStateException {
+ checkColumnIsAttached();
+ return state.width;
+ }
+
+ /**
+ * Sets the width (in pixels).
+ * <p>
+ * This overrides any configuration set by any of
+ * {@link #setExpandRatio(int)}, {@link #setMinimumWidth(double)} or
+ * {@link #setMaximumWidth(double)}.
+ *
+ * @param pixelWidth
+ * the new pixel width of the column
+ * @return the column itself
+ *
+ * @throws IllegalStateException
+ * if the column is no longer attached to any grid
+ * @throws IllegalArgumentException
+ * thrown if pixel width is less than zero
+ */
+ public Column setWidth(double pixelWidth)
+ throws IllegalStateException, IllegalArgumentException {
+ checkColumnIsAttached();
+ if (pixelWidth < 0) {
+ throw new IllegalArgumentException(
+ "Pixel width should be greated than 0 (in " + toString()
+ + ")");
+ }
+ if (state.width != pixelWidth) {
+ state.width = pixelWidth;
+ grid.markAsDirty();
+ grid.fireColumnResizeEvent(this, false);
+ }
+ return this;
+ }
+
+ /**
+ * Returns whether this column has an undefined width.
+ *
+ * @since 7.6
+ * @return whether the width is undefined
+ * @throws IllegalStateException
+ * if the column is no longer attached to any grid
+ */
+ public boolean isWidthUndefined() {
+ checkColumnIsAttached();
+ return state.width < 0;
+ }
+
+ /**
+ * Marks the column width as undefined. An undefined width means the
+ * grid is free to resize the column based on the cell contents and
+ * available space in the grid.
+ *
+ * @return the column itself
+ */
+ public Column setWidthUndefined() {
+ checkColumnIsAttached();
+ if (!isWidthUndefined()) {
+ state.width = -1;
+ grid.markAsDirty();
+ grid.fireColumnResizeEvent(this, false);
+ }
+ return this;
+ }
+
+ /**
+ * Checks if column is attached and throws an
+ * {@link IllegalStateException} if it is not
+ *
+ * @throws IllegalStateException
+ * if the column is no longer attached to any grid
+ */
+ protected void checkColumnIsAttached() throws IllegalStateException {
+ if (grid.getColumnByColumnId(state.id) == null) {
+ throw new IllegalStateException("Column no longer exists.");
+ }
+ }
+
+ /**
+ * Sets this column as the last frozen column in its grid.
+ *
+ * @return the column itself
+ *
+ * @throws IllegalArgumentException
+ * if the column is no longer attached to any grid
+ * @see LegacyGrid#setFrozenColumnCount(int)
+ */
+ public Column setLastFrozenColumn() {
+ checkColumnIsAttached();
+ grid.setFrozenColumnCount(
+ grid.getState(false).columnOrder.indexOf(getState().id)
+ + 1);
+ return this;
+ }
+
+ /**
+ * Sets the renderer for this column.
+ * <p>
+ * If a suitable converter isn't defined explicitly, the session
+ * converter factory is used to find a compatible converter.
+ *
+ * @param renderer
+ * the renderer to use
+ * @return the column itself
+ *
+ * @throws IllegalArgumentException
+ * if no compatible converter could be found
+ *
+ * @see VaadinSession#getConverterFactory()
+ * @see LegacyConverterUtil#getConverter(Class, Class, VaadinSession)
+ * @see #setConverter(LegacyConverter)
+ */
+ public Column setRenderer(Renderer<?> renderer) {
+ if (!internalSetRenderer(renderer)) {
+ throw new IllegalArgumentException(
+ "Could not find a converter for converting from the model type "
+ + getModelType()
+ + " to the renderer presentation type "
+ + renderer.getPresentationType() + " (in "
+ + toString() + ")");
+ }
+ return this;
+ }
+
+ /**
+ * Sets the renderer for this column and the converter used to convert
+ * from the property value type to the renderer presentation type.
+ *
+ * @param renderer
+ * the renderer to use, cannot be null
+ * @param converter
+ * the converter to use
+ * @return the column itself
+ *
+ * @throws IllegalArgumentException
+ * if the renderer is already associated with a grid column
+ */
+ public <T> Column setRenderer(Renderer<T> renderer,
+ LegacyConverter<? extends T, ?> converter) {
+ if (renderer.getParent() != null) {
+ throw new IllegalArgumentException(
+ "Cannot set a renderer that is already connected to a grid column (in "
+ + toString() + ")");
+ }
+
+ if (getRenderer() != null) {
+ grid.removeExtension(getRenderer());
+ }
+
+ grid.addRenderer(renderer);
+ state.rendererConnector = renderer;
+ setConverter(converter);
+ return this;
+ }
+
+ /**
+ * Sets the converter used to convert from the property value type to
+ * the renderer presentation type.
+ *
+ * @param converter
+ * the converter to use, or {@code null} to not use any
+ * converters
+ * @return the column itself
+ *
+ * @throws IllegalArgumentException
+ * if the types are not compatible
+ */
+ public Column setConverter(LegacyConverter<?, ?> converter)
+ throws IllegalArgumentException {
+ Class<?> modelType = getModelType();
+ if (converter != null) {
+ if (!converter.getModelType().isAssignableFrom(modelType)) {
+ throw new IllegalArgumentException(
+ "The converter model type "
+ + converter.getModelType()
+ + " is not compatible with the property type "
+ + modelType + " (in " + toString() + ")");
+
+ } else if (!getRenderer().getPresentationType()
+ .isAssignableFrom(converter.getPresentationType())) {
+ throw new IllegalArgumentException(
+ "The converter presentation type "
+ + converter.getPresentationType()
+ + " is not compatible with the renderer presentation type "
+ + getRenderer().getPresentationType()
+ + " (in " + toString() + ")");
+ }
+ }
+
+ else {
+ /*
+ * Since the converter is null (i.e. will be removed), we need
+ * to know that the renderer and model are compatible. If not,
+ * we can't allow for this to happen.
+ *
+ * The constructor is allowed to call this method with null
+ * without any compatibility checks, therefore we have a special
+ * case for it.
+ */
+
+ Class<?> rendererPresentationType = getRenderer()
+ .getPresentationType();
+ if (!isFirstConverterAssignment && !rendererPresentationType
+ .isAssignableFrom(modelType)) {
+ throw new IllegalArgumentException(
+ "Cannot remove converter, "
+ + "as renderer's presentation type "
+ + rendererPresentationType.getName()
+ + " and column's " + "model "
+ + modelType.getName() + " type aren't "
+ + "directly compatible with each other (in "
+ + toString() + ")");
+ }
+ }
+
+ isFirstConverterAssignment = false;
+
+ @SuppressWarnings("unchecked")
+ LegacyConverter<?, Object> castConverter = (LegacyConverter<?, Object>) converter;
+ this.converter = castConverter;
+
+ return this;
+ }
+
+ /**
+ * Returns the renderer instance used by this column.
+ *
+ * @return the renderer
+ */
+ public Renderer<?> getRenderer() {
+ return (Renderer<?>) getState().rendererConnector;
+ }
+
+ /**
+ * Returns the converter instance used by this column.
+ *
+ * @return the converter
+ */
+ public LegacyConverter<?, ?> getConverter() {
+ return converter;
+ }
+
+ private <T> boolean internalSetRenderer(Renderer<T> renderer) {
+
+ LegacyConverter<? extends T, ?> converter;
+ if (isCompatibleWithProperty(renderer, getConverter())) {
+ // Use the existing converter (possibly none) if types
+ // compatible
+ converter = (LegacyConverter<? extends T, ?>) getConverter();
+ } else {
+ converter = LegacyConverterUtil.getConverter(
+ renderer.getPresentationType(), getModelType(),
+ getSession());
+ }
+ setRenderer(renderer, converter);
+ return isCompatibleWithProperty(renderer, converter);
+ }
+
+ private VaadinSession getSession() {
+ UI ui = grid.getUI();
+ return ui != null ? ui.getSession() : null;
+ }
+
+ private boolean isCompatibleWithProperty(Renderer<?> renderer,
+ LegacyConverter<?, ?> converter) {
+ Class<?> type;
+ if (converter == null) {
+ type = getModelType();
+ } else {
+ type = converter.getPresentationType();
+ }
+ return renderer.getPresentationType().isAssignableFrom(type);
+ }
+
+ private Class<?> getModelType() {
+ return grid.getContainerDataSource()
+ .getType(grid.getPropertyIdByColumnId(state.id));
+ }
+
+ /**
+ * Sets whether this column is sortable by the user. The grid can be
+ * sorted by a sortable column by clicking or tapping the column's
+ * default header. Programmatic sorting using the Grid#sort methods is
+ * not affected by this setting.
+ *
+ * @param sortable
+ * {@code true} if the user should be able to sort the
+ * column, {@code false} otherwise
+ * @return the column itself
+ *
+ * @throws IllegalStateException
+ * if the data source of the Grid does not implement
+ * {@link Sortable}
+ * @throws IllegalStateException
+ * if the data source does not support sorting by the
+ * property associated with this column
+ */
+ public Column setSortable(boolean sortable) {
+ checkColumnIsAttached();
+
+ if (sortable) {
+ if (!(grid.datasource instanceof Sortable)) {
+ throw new IllegalStateException("Can't set column "
+ + toString()
+ + " sortable. The Container of Grid does not implement Sortable");
+ } else if (!((Sortable) grid.datasource)
+ .getSortableContainerPropertyIds()
+ .contains(propertyId)) {
+ throw new IllegalStateException(
+ "Can't set column " + toString()
+ + " sortable. Container doesn't support sorting by property "
+ + propertyId);
+ }
+ }
+
+ state.sortable = sortable;
+ grid.markAsDirty();
+ return this;
+ }
+
+ /**
+ * Returns whether the user can sort the grid by this column.
+ * <p>
+ * <em>Note:</em> it is possible to sort by this column programmatically
+ * using the Grid#sort methods regardless of the returned value.
+ *
+ * @return {@code true} if the column is sortable by the user,
+ * {@code false} otherwise
+ */
+ public boolean isSortable() {
+ return state.sortable;
+ }
+
+ @Override
+ public String toString() {
+ return getClass().getSimpleName() + "[propertyId:"
+ + grid.getPropertyIdByColumnId(state.id) + "]";
+ }
+
+ /**
+ * Sets the ratio with which the column expands.
+ * <p>
+ * By default, all columns expand equally (treated as if all of them had
+ * an expand ratio of 1). Once at least one column gets a defined expand
+ * ratio, the implicit expand ratio is removed, and only the defined
+ * expand ratios are taken into account.
+ * <p>
+ * If a column has a defined width ({@link #setWidth(double)}), it
+ * overrides this method's effects.
+ * <p>
+ * <em>Example:</em> A grid with three columns, with expand ratios 0, 1
+ * and 2, respectively. The column with a <strong>ratio of 0 is exactly
+ * as wide as its contents requires</strong>. The column with a ratio of
+ * 1 is as wide as it needs, <strong>plus a third of any excess
+ * space</strong>, because we have 3 parts total, and this column
+ * reserves only one of those. The column with a ratio of 2, is as wide
+ * as it needs to be, <strong>plus two thirds</strong> of the excess
+ * width.
+ *
+ * @param expandRatio
+ * the expand ratio of this column. {@code 0} to not have it
+ * expand at all. A negative number to clear the expand
+ * value.
+ * @throws IllegalStateException
+ * if the column is no longer attached to any grid
+ * @see #setWidth(double)
+ */
+ public Column setExpandRatio(int expandRatio)
+ throws IllegalStateException {
+ checkColumnIsAttached();
+
+ getState().expandRatio = expandRatio;
+ grid.markAsDirty();
+ return this;
+ }
+
+ /**
+ * Returns the column's expand ratio.
+ *
+ * @return the column's expand ratio
+ * @see #setExpandRatio(int)
+ */
+ public int getExpandRatio() {
+ return getState().expandRatio;
+ }
+
+ /**
+ * Clears the expand ratio for this column.
+ * <p>
+ * Equal to calling {@link #setExpandRatio(int) setExpandRatio(-1)}
+ *
+ * @throws IllegalStateException
+ * if the column is no longer attached to any grid
+ */
+ public Column clearExpandRatio() throws IllegalStateException {
+ return setExpandRatio(-1);
+ }
+
+ /**
+ * Sets the minimum width for this column.
+ * <p>
+ * This defines the minimum guaranteed pixel width of the column
+ * <em>when it is set to expand</em>.
+ *
+ * @throws IllegalStateException
+ * if the column is no longer attached to any grid
+ * @see #setExpandRatio(int)
+ */
+ public Column setMinimumWidth(double pixels)
+ throws IllegalStateException {
+ checkColumnIsAttached();
+
+ final double maxwidth = getMaximumWidth();
+ if (pixels >= 0 && pixels > maxwidth && maxwidth >= 0) {
+ throw new IllegalArgumentException("New minimum width ("
+ + pixels + ") was greater than maximum width ("
+ + maxwidth + ")");
+ }
+ getState().minWidth = pixels;
+ grid.markAsDirty();
+ return this;
+ }
+
+ /**
+ * Return the minimum width for this column.
+ *
+ * @return the minimum width for this column
+ * @see #setMinimumWidth(double)
+ */
+ public double getMinimumWidth() {
+ return getState().minWidth;
+ }
+
+ /**
+ * Sets the maximum width for this column.
+ * <p>
+ * This defines the maximum allowed pixel width of the column <em>when
+ * it is set to expand</em>.
+ *
+ * @param pixels
+ * the maximum width
+ * @throws IllegalStateException
+ * if the column is no longer attached to any grid
+ * @see #setExpandRatio(int)
+ */
+ public Column setMaximumWidth(double pixels) {
+ checkColumnIsAttached();
+
+ final double minwidth = getMinimumWidth();
+ if (pixels >= 0 && pixels < minwidth && minwidth >= 0) {
+ throw new IllegalArgumentException("New maximum width ("
+ + pixels + ") was less than minimum width (" + minwidth
+ + ")");
+ }
+
+ getState().maxWidth = pixels;
+ grid.markAsDirty();
+ return this;
+ }
+
+ /**
+ * Returns the maximum width for this column.
+ *
+ * @return the maximum width for this column
+ * @see #setMaximumWidth(double)
+ */
+ public double getMaximumWidth() {
+ return getState().maxWidth;
+ }
+
+ /**
+ * Sets whether the properties corresponding to this column should be
+ * editable when the item editor is active. By default columns are
+ * editable.
+ * <p>
+ * Values in non-editable columns are currently not displayed when the
+ * editor is active, but this will probably change in the future. They
+ * are not automatically assigned an editor field and, if one is
+ * manually assigned, it is not used. Columns that cannot (or should
+ * not) be edited even in principle should be set non-editable.
+ *
+ * @param editable
+ * {@code true} if this column should be editable,
+ * {@code false} otherwise
+ * @return this column
+ *
+ * @throws IllegalStateException
+ * if the editor is currently active
+ *
+ * @see LegacyGrid#editItem(Object)
+ * @see LegacyGrid#isEditorActive()
+ */
+ public Column setEditable(boolean editable) {
+ checkColumnIsAttached();
+ if (grid.isEditorActive()) {
+ throw new IllegalStateException(
+ "Cannot change column editable status while the editor is active");
+ }
+ getState().editable = editable;
+ grid.markAsDirty();
+ return this;
+ }
+
+ /**
+ * Returns whether the properties corresponding to this column should be
+ * editable when the item editor is active.
+ *
+ * @return {@code true} if this column is editable, {@code false}
+ * otherwise
+ *
+ * @see LegacyGrid#editItem(Object)
+ * @see #setEditable(boolean)
+ */
+
+ public boolean isEditable() {
+ return getState().editable;
+ }
+
+ /**
+ * Sets the field component used to edit the properties in this column
+ * when the item editor is active. If an item has not been set, then the
+ * binding is postponed until the item is set using
+ * {@link #editItem(Object)}.
+ * <p>
+ * Setting the field to <code>null</code> clears any previously set
+ * field, causing a new field to be created the next time the item
+ * editor is opened.
+ *
+ * @param editor
+ * the editor field
+ * @return this column
+ */
+ public Column setEditorField(LegacyField<?> editor) {
+ grid.setEditorField(getPropertyId(), editor);
+ return this;
+ }
+
+ /**
+ * Returns the editor field used to edit the properties in this column
+ * when the item editor is active. Returns null if the column is not
+ * {@link Column#isEditable() editable}.
+ * <p>
+ * When {@link #editItem(Object) editItem} is called, fields are
+ * automatically created and bound for any unbound properties.
+ * <p>
+ * Getting a field before the editor has been opened depends on special
+ * support from the {@link FieldGroup} in use. Using this method with a
+ * user-provided <code>FieldGroup</code> might cause
+ * {@link com.vaadin.data.fieldgroup.FieldGroup.BindException
+ * BindException} to be thrown.
+ *
+ * @return the bound field; or <code>null</code> if the respective
+ * column is not editable
+ *
+ * @throws IllegalArgumentException
+ * if there is no column for the provided property id
+ * @throws FieldGroup.BindException
+ * if no field has been configured and there is a problem
+ * building or binding
+ */
+ public LegacyField<?> getEditorField() {
+ return grid.getEditorField(getPropertyId());
+ }
+
+ /**
+ * Hides or shows the column. By default columns are visible before
+ * explicitly hiding them.
+ *
+ * @since 7.5.0
+ * @param hidden
+ * <code>true</code> to hide the column, <code>false</code>
+ * to show
+ * @return this column
+ */
+ public Column setHidden(boolean hidden) {
+ if (hidden != getState().hidden) {
+ getState().hidden = hidden;
+ grid.markAsDirty();
+ grid.fireColumnVisibilityChangeEvent(this, hidden, false);
+ }
+ return this;
+ }
+
+ /**
+ * Returns whether this column is hidden. Default is {@code false}.
+ *
+ * @since 7.5.0
+ * @return <code>true</code> if the column is currently hidden,
+ * <code>false</code> otherwise
+ */
+ public boolean isHidden() {
+ return getState().hidden;
+ }
+
+ /**
+ * Sets whether this column can be hidden by the user. Hidable columns
+ * can be hidden and shown via the sidebar menu.
+ *
+ * @since 7.5.0
+ * @param hidable
+ * <code>true</code> iff the column may be hidable by the
+ * user via UI interaction
+ * @return this column
+ */
+ public Column setHidable(boolean hidable) {
+ if (hidable != getState().hidable) {
+ getState().hidable = hidable;
+ grid.markAsDirty();
+ }
+ return this;
+ }
+
+ /**
+ * Returns whether this column can be hidden by the user. Default is
+ * {@code false}.
+ * <p>
+ * <em>Note:</em> the column can be programmatically hidden using
+ * {@link #setHidden(boolean)} regardless of the returned value.
+ *
+ * @since 7.5.0
+ * @return <code>true</code> if the user can hide the column,
+ * <code>false</code> if not
+ */
+ public boolean isHidable() {
+ return getState().hidable;
+ }
+
+ /**
+ * Sets whether this column can be resized by the user.
+ *
+ * @since 7.6
+ * @param resizable
+ * {@code true} if this column should be resizable,
+ * {@code false} otherwise
+ */
+ public Column setResizable(boolean resizable) {
+ if (resizable != getState().resizable) {
+ getState().resizable = resizable;
+ grid.markAsDirty();
+ }
+ return this;
+ }
+
+ /**
+ * Returns whether this column can be resized by the user. Default is
+ * {@code true}.
+ * <p>
+ * <em>Note:</em> the column can be programmatically resized using
+ * {@link #setWidth(double)} and {@link #setWidthUndefined()} regardless
+ * of the returned value.
+ *
+ * @since 7.6
+ * @return {@code true} if this column is resizable, {@code false}
+ * otherwise
+ */
+ public boolean isResizable() {
+ return getState().resizable;
+ }
+
+ /**
+ * Writes the design attributes for this column into given element.
+ *
+ * @since 7.5.0
+ *
+ * @param design
+ * Element to write attributes into
+ *
+ * @param designContext
+ * the design context
+ */
+ protected void writeDesign(Element design,
+ DesignContext designContext) {
+ Attributes attributes = design.attributes();
+ GridColumnState def = new GridColumnState();
+
+ DesignAttributeHandler.writeAttribute("property-id", attributes,
+ getPropertyId(), null, Object.class);
+
+ // Sortable is a special attribute that depends on the container.
+ DesignAttributeHandler.writeAttribute("sortable", attributes,
+ isSortable(), null, boolean.class);
+ DesignAttributeHandler.writeAttribute("editable", attributes,
+ isEditable(), def.editable, boolean.class);
+ DesignAttributeHandler.writeAttribute("resizable", attributes,
+ isResizable(), def.resizable, boolean.class);
+
+ DesignAttributeHandler.writeAttribute("hidable", attributes,
+ isHidable(), def.hidable, boolean.class);
+ DesignAttributeHandler.writeAttribute("hidden", attributes,
+ isHidden(), def.hidden, boolean.class);
+ DesignAttributeHandler.writeAttribute("hiding-toggle-caption",
+ attributes, getHidingToggleCaption(), null, String.class);
+
+ DesignAttributeHandler.writeAttribute("width", attributes,
+ getWidth(), def.width, Double.class);
+ DesignAttributeHandler.writeAttribute("min-width", attributes,
+ getMinimumWidth(), def.minWidth, Double.class);
+ DesignAttributeHandler.writeAttribute("max-width", attributes,
+ getMaximumWidth(), def.maxWidth, Double.class);
+ DesignAttributeHandler.writeAttribute("expand", attributes,
+ getExpandRatio(), def.expandRatio, Integer.class);
+ }
+
+ /**
+ * Reads the design attributes for this column from given element.
+ *
+ * @since 7.5.0
+ * @param design
+ * Element to read attributes from
+ * @param designContext
+ * the design context
+ */
+ protected void readDesign(Element design, DesignContext designContext) {
+ Attributes attributes = design.attributes();
+
+ if (design.hasAttr("sortable")) {
+ setSortable(DesignAttributeHandler.readAttribute("sortable",
+ attributes, boolean.class));
+ }
+ if (design.hasAttr("editable")) {
+ setEditable(DesignAttributeHandler.readAttribute("editable",
+ attributes, boolean.class));
+ }
+ if (design.hasAttr("resizable")) {
+ setResizable(DesignAttributeHandler.readAttribute("resizable",
+ attributes, boolean.class));
+ }
+
+ if (design.hasAttr("hidable")) {
+ setHidable(DesignAttributeHandler.readAttribute("hidable",
+ attributes, boolean.class));
+ }
+ if (design.hasAttr("hidden")) {
+ setHidden(DesignAttributeHandler.readAttribute("hidden",
+ attributes, boolean.class));
+ }
+ if (design.hasAttr("hiding-toggle-caption")) {
+ setHidingToggleCaption(DesignAttributeHandler.readAttribute(
+ "hiding-toggle-caption", attributes, String.class));
+ }
+
+ // Read size info where necessary.
+ if (design.hasAttr("width")) {
+ setWidth(DesignAttributeHandler.readAttribute("width",
+ attributes, Double.class));
+ }
+ if (design.hasAttr("min-width")) {
+ setMinimumWidth(DesignAttributeHandler
+ .readAttribute("min-width", attributes, Double.class));
+ }
+ if (design.hasAttr("max-width")) {
+ setMaximumWidth(DesignAttributeHandler
+ .readAttribute("max-width", attributes, Double.class));
+ }
+ if (design.hasAttr("expand")) {
+ if (design.attr("expand").isEmpty()) {
+ setExpandRatio(1);
+ } else {
+ setExpandRatio(DesignAttributeHandler.readAttribute(
+ "expand", attributes, Integer.class));
+ }
+ }
+ }
+ }
+
+ /**
+ * An abstract base class for server-side
+ * {@link com.vaadin.ui.renderers.Renderer Grid renderers}. This class
+ * currently extends the AbstractExtension superclass, but this fact should
+ * be regarded as an implementation detail and subject to change in a future
+ * major or minor Vaadin revision.
+ *
+ * @param <T>
+ * the type this renderer knows how to present
+ */
+ public static abstract class AbstractRenderer<T>
+ extends AbstractGridExtension implements Renderer<T> {
+
+ private final Class<T> presentationType;
+
+ private final String nullRepresentation;
+
+ protected AbstractRenderer(Class<T> presentationType,
+ String nullRepresentation) {
+ this.presentationType = presentationType;
+ this.nullRepresentation = nullRepresentation;
+ }
+
+ protected AbstractRenderer(Class<T> presentationType) {
+ this(presentationType, null);
+ }
+
+ /**
+ * This method is inherited from AbstractExtension but should never be
+ * called directly with an AbstractRenderer.
+ */
+ @Deprecated
+ @Override
+ protected Class<LegacyGrid> getSupportedParentType() {
+ return LegacyGrid.class;
+ }
+
+ /**
+ * This method is inherited from AbstractExtension but should never be
+ * called directly with an AbstractRenderer.
+ */
+ @Deprecated
+ @Override
+ protected void extend(AbstractClientConnector target) {
+ super.extend(target);
+ }
+
+ @Override
+ public Class<T> getPresentationType() {
+ return presentationType;
+ }
+
+ @Override
+ public JsonValue encode(T value) {
+ if (value == null) {
+ return encode(getNullRepresentation(), String.class);
+ } else {
+ return encode(value, getPresentationType());
+ }
+ }
+
+ /**
+ * Null representation for the renderer
+ *
+ * @return a textual representation of {@code null}
+ */
+ protected String getNullRepresentation() {
+ return nullRepresentation;
+ }
+
+ /**
+ * Encodes the given value to JSON.
+ * <p>
+ * This is a helper method that can be invoked by an
+ * {@link #encode(Object) encode(T)} override if serializing a value of
+ * type other than {@link #getPresentationType() the presentation type}
+ * is desired. For instance, a {@code Renderer<Date>} could first turn a
+ * date value into a formatted string and return
+ * {@code encode(dateString, String.class)}.
+ *
+ * @param value
+ * the value to be encoded
+ * @param type
+ * the type of the value
+ * @return a JSON representation of the given value
+ */
+ protected <U> JsonValue encode(U value, Class<U> type) {
+ return JsonCodec
+ .encode(value, null, type, getUI().getConnectorTracker())
+ .getEncodedValue();
+ }
+
+ /**
+ * Converts and encodes the given data model property value using the
+ * given converter and renderer. This method is public only for testing
+ * purposes.
+ *
+ * @since 7.6
+ * @param renderer
+ * the renderer to use
+ * @param converter
+ * the converter to use
+ * @param modelValue
+ * the value to convert and encode
+ * @param locale
+ * the locale to use in conversion
+ * @return an encoded value ready to be sent to the client
+ */
+ public static <T> JsonValue encodeValue(Object modelValue,
+ Renderer<T> renderer, LegacyConverter<?, ?> converter,
+ Locale locale) {
+ Class<T> presentationType = renderer.getPresentationType();
+ T presentationValue;
+
+ if (converter == null) {
+ try {
+ presentationValue = presentationType.cast(modelValue);
+ } catch (ClassCastException e) {
+ if (presentationType == String.class) {
+ // If there is no converter, just fallback to using
+ // toString(). modelValue can't be null as
+ // Class.cast(null) will always succeed
+ presentationValue = (T) modelValue.toString();
+ } else {
+ throw new LegacyConverter.ConversionException(
+ "Unable to convert value of type "
+ + modelValue.getClass().getName()
+ + " to presentation type "
+ + presentationType.getName()
+ + ". No converter is set and the types are not compatible.");
+ }
+ }
+ } else {
+ assert presentationType
+ .isAssignableFrom(converter.getPresentationType());
+ @SuppressWarnings("unchecked")
+ LegacyConverter<T, Object> safeConverter = (LegacyConverter<T, Object>) converter;
+ presentationValue = safeConverter.convertToPresentation(
+ modelValue, safeConverter.getPresentationType(),
+ locale);
+ }
+
+ JsonValue encodedValue;
+ try {
+ encodedValue = renderer.encode(presentationValue);
+ } catch (Exception e) {
+ getLogger().log(Level.SEVERE, "Unable to encode data", e);
+ encodedValue = renderer.encode(null);
+ }
+
+ return encodedValue;
+ }
+
+ private static Logger getLogger() {
+ return Logger.getLogger(AbstractRenderer.class.getName());
+ }
+
+ }
+
+ /**
+ * An abstract base class for server-side Grid extensions.
+ * <p>
+ * Note: If the extension is an instance of {@link DataGenerator} it will
+ * automatically register itself to {@link RpcDataProviderExtension} of
+ * extended Grid. On remove this registration is automatically removed.
+ *
+ * @since 7.5
+ */
+ public static abstract class AbstractGridExtension
+ extends AbstractExtension {
+
+ /**
+ * Constructs a new Grid extension.
+ */
+ public AbstractGridExtension() {
+ super();
+ }
+
+ /**
+ * Constructs a new Grid extension and extends given Grid.
+ *
+ * @param grid
+ * a grid instance
+ */
+ public AbstractGridExtension(LegacyGrid grid) {
+ super();
+ extend(grid);
+ }
+
+ @Override
+ protected void extend(AbstractClientConnector target) {
+ super.extend(target);
+
+ if (this instanceof DataGenerator) {
+ getParentGrid().datasourceExtension
+ .addDataGenerator((DataGenerator) this);
+ }
+ }
+
+ @Override
+ public void remove() {
+ if (this instanceof DataGenerator) {
+ getParentGrid().datasourceExtension
+ .removeDataGenerator((DataGenerator) this);
+ }
+
+ super.remove();
+ }
+
+ /**
+ * Gets the item id for a row key.
+ * <p>
+ * A key is used to identify a particular row on both a server and a
+ * client. This method can be used to get the item id for the row key
+ * that the client has sent.
+ *
+ * @param rowKey
+ * the row key for which to retrieve an item id
+ * @return the item id corresponding to {@code key}
+ */
+ protected Object getItemId(String rowKey) {
+ return getParentGrid().getKeyMapper().get(rowKey);
+ }
+
+ /**
+ * Gets the column for a column id.
+ * <p>
+ * An id is used to identify a particular column on both a server and a
+ * client. This method can be used to get the column for the column id
+ * that the client has sent.
+ *
+ * @param columnId
+ * the column id for which to retrieve a column
+ * @return the column corresponding to {@code columnId}
+ */
+ protected Column getColumn(String columnId) {
+ return getParentGrid().getColumnByColumnId(columnId);
+ }
+
+ /**
+ * Gets the parent Grid of the renderer.
+ *
+ * @return parent grid
+ * @throws IllegalStateException
+ * if parent is not Grid
+ */
+ protected LegacyGrid getParentGrid() {
+ if (getParent() instanceof LegacyGrid) {
+ LegacyGrid grid = (LegacyGrid) getParent();
+ return grid;
+ } else if (getParent() == null) {
+ throw new IllegalStateException(
+ "Renderer is not attached to any parent");
+ } else {
+ throw new IllegalStateException(
+ "Renderers can be used only with Grid. Extended "
+ + getParent().getClass().getSimpleName()
+ + " instead");
+ }
+ }
+
+ /**
+ * Resends the row data for given item id to the client.
+ *
+ * @since 7.6
+ * @param itemId
+ * row to refresh
+ */
+ protected void refreshRow(Object itemId) {
+ getParentGrid().datasourceExtension.updateRowData(itemId);
+ }
+
+ /**
+ * Informs the parent Grid that this Extension wants to add a child
+ * component to it.
+ *
+ * @since 7.6
+ * @param c
+ * component
+ */
+ protected void addComponentToGrid(Component c) {
+ getParentGrid().addComponent(c);
+ }
+
+ /**
+ * Informs the parent Grid that this Extension wants to remove a child
+ * component from it.
+ *
+ * @since 7.6
+ * @param c
+ * component
+ */
+ protected void removeComponentFromGrid(Component c) {
+ getParentGrid().removeComponent(c);
+ }
+ }
+
+ /**
+ * The data source attached to the grid
+ */
+ private Container.Indexed datasource;
+
+ /**
+ * Property id to column instance mapping
+ */
+ private final Map<Object, Column> columns = new HashMap<>();
+
+ /**
+ * Key generator for column server-to-client communication
+ */
+ private final KeyMapper<Object> columnKeys = new KeyMapper<>();
+
+ /**
+ * The current sort order
+ */
+ private final List<SortOrder> sortOrder = new ArrayList<>();
+
+ /**
+ * Property listener for listening to changes in data source properties.
+ */
+ private final PropertySetChangeListener propertyListener = new PropertySetChangeListener() {
+
+ @Override
+ public void containerPropertySetChange(PropertySetChangeEvent event) {
+ Collection<?> properties = new HashSet<Object>(
+ event.getContainer().getContainerPropertyIds());
+
+ // Find columns that need to be removed.
+ List<Column> removedColumns = new LinkedList<>();
+ for (Object propertyId : columns.keySet()) {
+ if (!properties.contains(propertyId)) {
+ removedColumns.add(getColumn(propertyId));
+ }
+ }
+
+ // Actually remove columns.
+ for (Column column : removedColumns) {
+ Object propertyId = column.getPropertyId();
+ internalRemoveColumn(propertyId);
+ columnKeys.remove(propertyId);
+ }
+ datasourceExtension.columnsRemoved(removedColumns);
+
+ // Add new columns
+ List<Column> addedColumns = new LinkedList<>();
+ for (Object propertyId : properties) {
+ if (!columns.containsKey(propertyId)) {
+ addedColumns.add(appendColumn(propertyId));
+ }
+ }
+ datasourceExtension.columnsAdded(addedColumns);
+
+ if (getFrozenColumnCount() > columns.size()) {
+ setFrozenColumnCount(columns.size());
+ }
+
+ // Unset sortable for non-sortable columns.
+ if (datasource instanceof Sortable) {
+ Collection<?> sortables = ((Sortable) datasource)
+ .getSortableContainerPropertyIds();
+ for (Object propertyId : columns.keySet()) {
+ Column column = columns.get(propertyId);
+ if (!sortables.contains(propertyId)
+ && column.isSortable()) {
+ column.setSortable(false);
+ }
+ }
+ }
+ }
+ };
+
+ private final ItemSetChangeListener editorClosingItemSetListener = new ItemSetChangeListener() {
+ @Override
+ public void containerItemSetChange(ItemSetChangeEvent event) {
+ cancelEditor();
+ }
+ };
+
+ private RpcDataProviderExtension datasourceExtension;
+
+ /**
+ * The selection model that is currently in use. Never <code>null</code>
+ * after the constructor has been run.
+ */
+ private SelectionModel selectionModel;
+
+ /**
+ * Used to know whether selection change events originate from the server or
+ * the client so the selection change handler knows whether the changes
+ * should be sent to the client.
+ */
+ private boolean applyingSelectionFromClient;
+
+ private final Header header = new Header(this);
+ private final Footer footer = new Footer(this);
+
+ private Object editedItemId = null;
+ private boolean editorActive = false;
+ private FieldGroup editorFieldGroup = new CustomFieldGroup();
+
+ private CellStyleGenerator cellStyleGenerator;
+ private RowStyleGenerator rowStyleGenerator;
+
+ private CellDescriptionGenerator cellDescriptionGenerator;
+ private RowDescriptionGenerator rowDescriptionGenerator;
+
+ /**
+ * <code>true</code> if Grid is using the internal IndexedContainer created
+ * in Grid() constructor, or <code>false</code> if the user has set their
+ * own Container.
+ *
+ * @see #setContainerDataSource(Indexed)
+ * @see #LegacyGrid()
+ */
+ private boolean defaultContainer = true;
+
+ private EditorErrorHandler editorErrorHandler = new DefaultEditorErrorHandler();
+
+ private DetailComponentManager detailComponentManager = null;
+
+ private Set<Component> extensionComponents = new HashSet<>();
+
+ private static final Method SELECTION_CHANGE_METHOD = ReflectTools
+ .findMethod(SelectionListener.class, "select",
+ SelectionEvent.class);
+
+ private static final Method SORT_ORDER_CHANGE_METHOD = ReflectTools
+ .findMethod(SortListener.class, "sort", SortEvent.class);
+
+ private static final Method COLUMN_REORDER_METHOD = ReflectTools.findMethod(
+ ColumnReorderListener.class, "columnReorder",
+ ColumnReorderEvent.class);
+
+ private static final Method COLUMN_RESIZE_METHOD = ReflectTools.findMethod(
+ ColumnResizeListener.class, "columnResize",
+ ColumnResizeEvent.class);
+
+ private static final Method COLUMN_VISIBILITY_METHOD = ReflectTools
+ .findMethod(ColumnVisibilityChangeListener.class,
+ "columnVisibilityChanged",
+ ColumnVisibilityChangeEvent.class);
+
+ /**
+ * Creates a new Grid with a new {@link IndexedContainer} as the data
+ * source.
+ */
+ public LegacyGrid() {
+ this(null, null);
+ }
+
+ /**
+ * Creates a new Grid using the given data source.
+ *
+ * @param dataSource
+ * the indexed container to use as a data source
+ */
+ public LegacyGrid(final Container.Indexed dataSource) {
+ this(null, dataSource);
+ }
+
+ /**
+ * Creates a new Grid with the given caption and a new
+ * {@link IndexedContainer} data source.
+ *
+ * @param caption
+ * the caption of the grid
+ */
+ public LegacyGrid(String caption) {
+ this(caption, null);
+ }
+
+ /**
+ * Creates a new Grid with the given caption and data source. If the data
+ * source is null, a new {@link IndexedContainer} will be used.
+ *
+ * @param caption
+ * the caption of the grid
+ * @param dataSource
+ * the indexed container to use as a data source
+ */
+ public LegacyGrid(String caption, Container.Indexed dataSource) {
+ if (dataSource == null) {
+ internalSetContainerDataSource(new IndexedContainer());
+ } else {
+ setContainerDataSource(dataSource);
+ }
+ setCaption(caption);
+ initGrid();
+ }
+
+ /**
+ * Grid initial setup
+ */
+ private void initGrid() {
+ setSelectionMode(getDefaultSelectionMode());
+
+ registerRpc(new GridServerRpc() {
+
+ @Override
+ public void sort(String[] columnIds, SortDirection[] directions,
+ boolean userOriginated) {
+ assert columnIds.length == directions.length;
+
+ List<SortOrder> order = new ArrayList<>(columnIds.length);
+ for (int i = 0; i < columnIds.length; i++) {
+ Object propertyId = getPropertyIdByColumnId(columnIds[i]);
+ order.add(new SortOrder(propertyId, directions[i]));
+ }
+ setSortOrder(order, userOriginated);
+ if (!order.equals(getSortOrder())) {
+ /*
+ * Actual sort order is not what the client expects. Make
+ * sure the client gets a state change event by clearing the
+ * diffstate and marking as dirty
+ */
+ ConnectorTracker connectorTracker = getUI()
+ .getConnectorTracker();
+ JsonObject diffState = connectorTracker
+ .getDiffState(LegacyGrid.this);
+ diffState.remove("sortColumns");
+ diffState.remove("sortDirs");
+ markAsDirty();
+ }
+ }
+
+ @Override
+ public void itemClick(String rowKey, String columnId,
+ MouseEventDetails details) {
+ Object itemId = getKeyMapper().get(rowKey);
+ Item item = datasource.getItem(itemId);
+ Object propertyId = getPropertyIdByColumnId(columnId);
+ fireEvent(new ItemClickEvent(LegacyGrid.this, item, itemId,
+ propertyId, details));
+ }
+
+ @Override
+ public void columnsReordered(List<String> newColumnOrder,
+ List<String> oldColumnOrder) {
+ final String diffStateKey = "columnOrder";
+ ConnectorTracker connectorTracker = getUI()
+ .getConnectorTracker();
+ JsonObject diffState = connectorTracker.getDiffState(LegacyGrid.this);
+ // discard the change if the columns have been reordered from
+ // the server side, as the server side is always right
+ if (getState(false).columnOrder.equals(oldColumnOrder)) {
+ // Don't mark as dirty since client has the state already
+ getState(false).columnOrder = newColumnOrder;
+ // write changes to diffState so that possible reverting the
+ // column order is sent to client
+ assert diffState
+ .hasKey(diffStateKey) : "Field name has changed";
+ Type type = null;
+ try {
+ type = (getState(false).getClass()
+ .getDeclaredField(diffStateKey)
+ .getGenericType());
+ } catch (NoSuchFieldException e) {
+ e.printStackTrace();
+ } catch (SecurityException e) {
+ e.printStackTrace();
+ }
+ EncodeResult encodeResult = JsonCodec.encode(
+ getState(false).columnOrder, diffState, type,
+ connectorTracker);
+
+ diffState.put(diffStateKey, encodeResult.getEncodedValue());
+ fireColumnReorderEvent(true);
+ } else {
+ // make sure the client is reverted to the order that the
+ // server thinks it is
+ diffState.remove(diffStateKey);
+ markAsDirty();
+ }
+ }
+
+ @Override
+ public void columnVisibilityChanged(String id, boolean hidden,
+ boolean userOriginated) {
+ final Column column = getColumnByColumnId(id);
+ final GridColumnState columnState = column.getState();
+
+ if (columnState.hidden != hidden) {
+ columnState.hidden = hidden;
+
+ final String diffStateKey = "columns";
+ ConnectorTracker connectorTracker = getUI()
+ .getConnectorTracker();
+ JsonObject diffState = connectorTracker
+ .getDiffState(LegacyGrid.this);
+
+ assert diffState
+ .hasKey(diffStateKey) : "Field name has changed";
+ Type type = null;
+ try {
+ type = (getState(false).getClass()
+ .getDeclaredField(diffStateKey)
+ .getGenericType());
+ } catch (NoSuchFieldException e) {
+ e.printStackTrace();
+ } catch (SecurityException e) {
+ e.printStackTrace();
+ }
+ EncodeResult encodeResult = JsonCodec.encode(
+ getState(false).columns, diffState, type,
+ connectorTracker);
+
+ diffState.put(diffStateKey, encodeResult.getEncodedValue());
+
+ fireColumnVisibilityChangeEvent(column, hidden,
+ userOriginated);
+ }
+ }
+
+ @Override
+ public void contextClick(int rowIndex, String rowKey,
+ String columnId, Section section,
+ MouseEventDetails details) {
+ Object itemId = null;
+ if (rowKey != null) {
+ itemId = getKeyMapper().get(rowKey);
+ }
+ fireEvent(new GridContextClickEvent(LegacyGrid.this, details, section,
+ rowIndex, itemId, getPropertyIdByColumnId(columnId)));
+ }
+
+ @Override
+ public void columnResized(String id, double pixels) {
+ final Column column = getColumnByColumnId(id);
+ if (column != null && column.isResizable()) {
+ column.getState().width = pixels;
+ fireColumnResizeEvent(column, true);
+ markAsDirty();
+ }
+ }
+ });
+
+ registerRpc(new EditorServerRpc() {
+
+ @Override
+ public void bind(int rowIndex) {
+ try {
+ Object id = getContainerDataSource().getIdByIndex(rowIndex);
+
+ final boolean opening = editedItemId == null;
+
+ final boolean moving = !opening && !editedItemId.equals(id);
+
+ final boolean allowMove = !isEditorBuffered()
+ && getEditorFieldGroup().isValid();
+
+ if (opening || !moving || allowMove) {
+ doBind(id);
+ } else {
+ failBind(null);
+ }
+ } catch (Exception e) {
+ failBind(e);
+ }
+ }
+
+ private void doBind(Object id) {
+ editedItemId = id;
+ doEditItem();
+ getEditorRpc().confirmBind(true);
+ }
+
+ private void failBind(Exception e) {
+ if (e != null) {
+ handleError(e);
+ }
+ getEditorRpc().confirmBind(false);
+ }
+
+ @Override
+ public void cancel(int rowIndex) {
+ try {
+ // For future proofing even though cannot currently fail
+ doCancelEditor();
+ } catch (Exception e) {
+ handleError(e);
+ }
+ }
+
+ @Override
+ public void save(int rowIndex) {
+ List<String> errorColumnIds = null;
+ String errorMessage = null;
+ boolean success = false;
+ try {
+ saveEditor();
+ success = true;
+ } catch (CommitException e) {
+ try {
+ CommitErrorEvent event = new CommitErrorEvent(LegacyGrid.this,
+ e);
+ getEditorErrorHandler().commitError(event);
+
+ errorMessage = event.getUserErrorMessage();
+
+ errorColumnIds = new ArrayList<>();
+ for (Column column : event.getErrorColumns()) {
+ errorColumnIds.add(column.state.id);
+ }
+ } catch (Exception ee) {
+ // A badly written error handler can throw an exception,
+ // which would lock up the Grid
+ handleError(ee);
+ }
+ } catch (Exception e) {
+ handleError(e);
+ }
+ getEditorRpc().confirmSave(success, errorMessage,
+ errorColumnIds);
+ }
+
+ private void handleError(Exception e) {
+ com.vaadin.server.ErrorEvent.findErrorHandler(LegacyGrid.this)
+ .error(new ConnectorErrorEvent(LegacyGrid.this, e));
+ }
+ });
+ }
+
+ @Override
+ public void beforeClientResponse(boolean initial) {
+ try {
+ header.sanityCheck();
+ footer.sanityCheck();
+ } catch (Exception e) {
+ e.printStackTrace();
+ setComponentError(new ErrorMessage() {
+
+ @Override
+ public ErrorLevel getErrorLevel() {
+ return ErrorLevel.CRITICAL;
+ }
+
+ @Override
+ public String getFormattedHtmlMessage() {
+ return "Incorrectly merged cells";
+ }
+
+ });
+ }
+
+ super.beforeClientResponse(initial);
+ }
+
+ /**
+ * Sets the grid data source.
+ * <p>
+ *
+ * <strong>Note</strong> Grid columns are based on properties and try to
+ * detect a correct converter for the data type. The columns are not
+ * reinitialized automatically if the container is changed, and if the same
+ * properties are present after container change, the columns are reused.
+ * Properties with same names, but different data types will lead to
+ * unpredictable behaviour.
+ *
+ * @param container
+ * The container data source. Cannot be null.
+ * @throws IllegalArgumentException
+ * if the data source is null
+ */
+ public void setContainerDataSource(Container.Indexed container) {
+ defaultContainer = false;
+ internalSetContainerDataSource(container);
+ }
+
+ private void internalSetContainerDataSource(Container.Indexed container) {
+ if (container == null) {
+ throw new IllegalArgumentException(
+ "Cannot set the datasource to null");
+ }
+ if (datasource == container) {
+ return;
+ }
+
+ // Remove old listeners
+ if (datasource instanceof PropertySetChangeNotifier) {
+ ((PropertySetChangeNotifier) datasource)
+ .removePropertySetChangeListener(propertyListener);
+ }
+
+ if (datasourceExtension != null) {
+ removeExtension(datasourceExtension);
+ }
+
+ // Remove old DetailComponentManager
+ if (detailComponentManager != null) {
+ detailComponentManager.remove();
+ }
+
+ resetEditor();
+
+ datasource = container;
+
+ //
+ // Adjust sort order
+ //
+
+ if (container instanceof Container.Sortable) {
+
+ // If the container is sortable, go through the current sort order
+ // and match each item to the sortable properties of the new
+ // container. If the new container does not support an item in the
+ // current sort order, that item is removed from the current sort
+ // order list.
+ Collection<?> sortableProps = ((Container.Sortable) getContainerDataSource())
+ .getSortableContainerPropertyIds();
+
+ Iterator<SortOrder> i = sortOrder.iterator();
+ while (i.hasNext()) {
+ if (!sortableProps.contains(i.next().getPropertyId())) {
+ i.remove();
+ }
+ }
+
+ sort(false);
+ } else {
+ // Clear sorting order. Don't sort.
+ sortOrder.clear();
+ }
+
+ datasourceExtension = new RpcDataProviderExtension(container);
+ datasourceExtension.extend(this);
+ datasourceExtension.addDataGenerator(new RowDataGenerator());
+ for (Extension e : getExtensions()) {
+ if (e instanceof DataGenerator) {
+ datasourceExtension.addDataGenerator((DataGenerator) e);
+ }
+ }
+
+ if (detailComponentManager != null) {
+ detailComponentManager = new DetailComponentManager(this,
+ detailComponentManager.getDetailsGenerator());
+ } else {
+ detailComponentManager = new DetailComponentManager(this);
+ }
+
+ /*
+ * selectionModel == null when the invocation comes from the
+ * constructor.
+ */
+ if (selectionModel != null) {
+ selectionModel.reset();
+ }
+
+ // Listen to changes in properties and remove columns if needed
+ if (datasource instanceof PropertySetChangeNotifier) {
+ ((PropertySetChangeNotifier) datasource)
+ .addPropertySetChangeListener(propertyListener);
+ }
+
+ /*
+ * activeRowHandler will be updated by the client-side request that
+ * occurs on container change - no need to actively re-insert any
+ * ValueChangeListeners at this point.
+ */
+
+ setFrozenColumnCount(0);
+
+ if (columns.isEmpty()) {
+ // Add columns
+ for (Object propertyId : datasource.getContainerPropertyIds()) {
+ Column column = appendColumn(propertyId);
+
+ // Initial sorting is defined by container
+ if (datasource instanceof Sortable) {
+ column.setSortable(((Sortable) datasource)
+ .getSortableContainerPropertyIds()
+ .contains(propertyId));
+ } else {
+ column.setSortable(false);
+ }
+ }
+ } else {
+ Collection<?> properties = datasource.getContainerPropertyIds();
+ for (Object property : columns.keySet()) {
+ if (!properties.contains(property)) {
+ throw new IllegalStateException(
+ "Found at least one column in Grid that does not exist in the given container: "
+ + property + " with the header \""
+ + getColumn(property).getHeaderCaption()
+ + "\". "
+ + "Call removeAllColumns() before setContainerDataSource() if you want to reconfigure the columns based on the new container.");
+ }
+
+ if (!(datasource instanceof Sortable)
+ || !((Sortable) datasource)
+ .getSortableContainerPropertyIds()
+ .contains(property)) {
+ columns.get(property).setSortable(false);
+ }
+ }
+ }
+ }
+
+ /**
+ * Returns the grid data source.
+ *
+ * @return the container data source of the grid
+ */
+ public Container.Indexed getContainerDataSource() {
+ return datasource;
+ }
+
+ /**
+ * Returns a column based on the property id
+ *
+ * @param propertyId
+ * the property id of the column
+ * @return the column or <code>null</code> if not found
+ */
+ public Column getColumn(Object propertyId) {
+ return columns.get(propertyId);
+ }
+
+ /**
+ * Returns a copy of currently configures columns in their current visual
+ * order in this Grid.
+ *
+ * @return unmodifiable copy of current columns in visual order
+ */
+ public List<Column> getColumns() {
+ List<Column> columns = new ArrayList<>();
+ for (String columnId : getState(false).columnOrder) {
+ columns.add(getColumnByColumnId(columnId));
+ }
+ return Collections.unmodifiableList(columns);
+ }
+
+ /**
+ * Adds a new Column to Grid. Also adds the property to container with data
+ * type String, if property for column does not exist in it. Default value
+ * for the new property is an empty String.
+ * <p>
+ * Note that adding a new property is only done for the default container
+ * that Grid sets up with the default constructor.
+ *
+ * @param propertyId
+ * the property id of the new column
+ * @return the new column
+ *
+ * @throws IllegalStateException
+ * if column for given property already exists in this grid
+ */
+
+ public Column addColumn(Object propertyId) throws IllegalStateException {
+ if (datasource.getContainerPropertyIds().contains(propertyId)
+ && !columns.containsKey(propertyId)) {
+ appendColumn(propertyId);
+ } else if (defaultContainer) {
+ addColumnProperty(propertyId, String.class, "");
+ } else {
+ if (columns.containsKey(propertyId)) {
+ throw new IllegalStateException(
+ "A column for property id '" + propertyId.toString()
+ + "' already exists in this grid");
+ } else {
+ throw new IllegalStateException(
+ "Property id '" + propertyId.toString()
+ + "' does not exist in the container");
+ }
+ }
+
+ // Inform the data provider of this new column.
+ Column column = getColumn(propertyId);
+ List<Column> addedColumns = new ArrayList<>();
+ addedColumns.add(column);
+ datasourceExtension.columnsAdded(addedColumns);
+
+ return column;
+ }
+
+ /**
+ * Adds a new Column to Grid. This function makes sure that the property
+ * with the given id and data type exists in the container. If property does
+ * not exists, it will be created.
+ * <p>
+ * Default value for the new property is 0 if type is Integer, Double and
+ * Float. If type is String, default value is an empty string. For all other
+ * types the default value is null.
+ * <p>
+ * Note that adding a new property is only done for the default container
+ * that Grid sets up with the default constructor.
+ *
+ * @param propertyId
+ * the property id of the new column
+ * @param type
+ * the data type for the new property
+ * @return the new column
+ *
+ * @throws IllegalStateException
+ * if column for given property already exists in this grid or
+ * property already exists in the container with wrong type
+ */
+ public Column addColumn(Object propertyId, Class<?> type) {
+ addColumnProperty(propertyId, type, null);
+ return getColumn(propertyId);
+ }
+
+ protected void addColumnProperty(Object propertyId, Class<?> type,
+ Object defaultValue) throws IllegalStateException {
+ if (!defaultContainer) {
+ throw new IllegalStateException(
+ "Container for this Grid is not a default container from Grid() constructor");
+ }
+
+ if (!columns.containsKey(propertyId)) {
+ if (!datasource.getContainerPropertyIds().contains(propertyId)) {
+ datasource.addContainerProperty(propertyId, type, defaultValue);
+ } else {
+ Property<?> containerProperty = datasource.getContainerProperty(
+ datasource.firstItemId(), propertyId);
+ if (containerProperty.getType() == type) {
+ appendColumn(propertyId);
+ } else {
+ throw new IllegalStateException(
+ "DataSource already has the given property "
+ + propertyId + " with a different type");
+ }
+ }
+ } else {
+ throw new IllegalStateException(
+ "Grid already has a column for property " + propertyId);
+ }
+ }
+
+ /**
+ * Removes all columns from this Grid.
+ */
+ public void removeAllColumns() {
+ List<Column> removed = new ArrayList<>(columns.values());
+ Set<Object> properties = new HashSet<>(columns.keySet());
+ for (Object propertyId : properties) {
+ removeColumn(propertyId);
+ }
+ datasourceExtension.columnsRemoved(removed);
+ }
+
+ /**
+ * Used internally by the {@link LegacyGrid} to get a {@link Column} by
+ * referencing its generated state id. Also used by {@link Column} to verify
+ * if it has been detached from the {@link LegacyGrid}.
+ *
+ * @param columnId
+ * the client id generated for the column when the column is
+ * added to the grid
+ * @return the column with the id or <code>null</code> if not found
+ */
+ Column getColumnByColumnId(String columnId) {
+ Object propertyId = getPropertyIdByColumnId(columnId);
+ return getColumn(propertyId);
+ }
+
+ /**
+ * Used internally by the {@link LegacyGrid} to get a property id by referencing
+ * the columns generated state id.
+ *
+ * @param columnId
+ * The state id of the column
+ * @return The column instance or null if not found
+ */
+ Object getPropertyIdByColumnId(String columnId) {
+ return columnKeys.get(columnId);
+ }
+
+ /**
+ * Returns whether column reordering is allowed. Default value is
+ * <code>false</code>.
+ *
+ * @since 7.5.0
+ * @return true if reordering is allowed
+ */
+ public boolean isColumnReorderingAllowed() {
+ return getState(false).columnReorderingAllowed;
+ }
+
+ /**
+ * Sets whether or not column reordering is allowed. Default value is
+ * <code>false</code>.
+ *
+ * @since 7.5.0
+ * @param columnReorderingAllowed
+ * specifies whether column reordering is allowed
+ */
+ public void setColumnReorderingAllowed(boolean columnReorderingAllowed) {
+ if (isColumnReorderingAllowed() != columnReorderingAllowed) {
+ getState().columnReorderingAllowed = columnReorderingAllowed;
+ }
+ }
+
+ @Override
+ protected GridState getState() {
+ return (GridState) super.getState();
+ }
+
+ @Override
+ protected GridState getState(boolean markAsDirty) {
+ return (GridState) super.getState(markAsDirty);
+ }
+
+ /**
+ * Creates a new column based on a property id and appends it as the last
+ * column.
+ *
+ * @param datasourcePropertyId
+ * The property id of a property in the datasource
+ */
+ private Column appendColumn(Object datasourcePropertyId) {
+ if (datasourcePropertyId == null) {
+ throw new IllegalArgumentException("Property id cannot be null");
+ }
+ assert datasource.getContainerPropertyIds().contains(
+ datasourcePropertyId) : "Datasource should contain the property id";
+
+ GridColumnState columnState = new GridColumnState();
+ columnState.id = columnKeys.key(datasourcePropertyId);
+
+ Column column = new Column(this, columnState, datasourcePropertyId);
+ columns.put(datasourcePropertyId, column);
+
+ getState().columns.add(columnState);
+ getState().columnOrder.add(columnState.id);
+ header.addColumn(datasourcePropertyId);
+ footer.addColumn(datasourcePropertyId);
+
+ String humanFriendlyPropertyId = SharedUtil.propertyIdToHumanFriendly(
+ String.valueOf(datasourcePropertyId));
+ column.setHeaderCaption(humanFriendlyPropertyId);
+
+ if (datasource instanceof Sortable
+ && ((Sortable) datasource).getSortableContainerPropertyIds()
+ .contains(datasourcePropertyId)) {
+ column.setSortable(true);
+ }
+
+ return column;
+ }
+
+ /**
+ * Removes a column from Grid based on a property id.
+ *
+ * @param propertyId
+ * The property id of column to be removed
+ *
+ * @throws IllegalArgumentException
+ * if there is no column for given property id in this grid
+ */
+ public void removeColumn(Object propertyId)
+ throws IllegalArgumentException {
+ if (!columns.keySet().contains(propertyId)) {
+ throw new IllegalArgumentException(
+ "There is no column for given property id " + propertyId);
+ }
+
+ List<Column> removed = new ArrayList<>();
+ removed.add(getColumn(propertyId));
+ internalRemoveColumn(propertyId);
+ datasourceExtension.columnsRemoved(removed);
+ }
+
+ private void internalRemoveColumn(Object propertyId) {
+ setEditorField(propertyId, null);
+ header.removeColumn(propertyId);
+ footer.removeColumn(propertyId);
+ Column column = columns.remove(propertyId);
+ getState().columnOrder.remove(columnKeys.key(propertyId));
+ getState().columns.remove(column.getState());
+ removeExtension(column.getRenderer());
+ }
+
+ /**
+ * Sets the columns and their order for the grid. Current columns whose
+ * property id is not in propertyIds are removed. Similarly, a column is
+ * added for any property id in propertyIds that has no corresponding column
+ * in this Grid.
+ *
+ * @since 7.5.0
+ *
+ * @param propertyIds
+ * properties in the desired column order
+ */
+ public void setColumns(Object... propertyIds) {
+ Set<?> removePids = new HashSet<>(columns.keySet());
+ removePids.removeAll(Arrays.asList(propertyIds));
+ for (Object removePid : removePids) {
+ removeColumn(removePid);
+ }
+ Set<?> addPids = new HashSet<>(Arrays.asList(propertyIds));
+ addPids.removeAll(columns.keySet());
+ for (Object propertyId : addPids) {
+ addColumn(propertyId);
+ }
+ setColumnOrder(propertyIds);
+ }
+
+ /**
+ * Sets a new column order for the grid. All columns which are not ordered
+ * here will remain in the order they were before as the last columns of
+ * grid.
+ *
+ * @param propertyIds
+ * properties in the order columns should be
+ */
+ public void setColumnOrder(Object... propertyIds) {
+ List<String> columnOrder = new ArrayList<>();
+ for (Object propertyId : propertyIds) {
+ if (columns.containsKey(propertyId)) {
+ columnOrder.add(columnKeys.key(propertyId));
+ } else {
+ throw new IllegalArgumentException(
+ "Grid does not contain column for property "
+ + String.valueOf(propertyId));
+ }
+ }
+
+ List<String> stateColumnOrder = getState().columnOrder;
+ if (stateColumnOrder.size() != columnOrder.size()) {
+ stateColumnOrder.removeAll(columnOrder);
+ columnOrder.addAll(stateColumnOrder);
+ }
+ getState().columnOrder = columnOrder;
+ fireColumnReorderEvent(false);
+ }
+
+ /**
+ * Sets the number of frozen columns in this grid. Setting the count to 0
+ * means that no data columns will be frozen, but the built-in selection
+ * checkbox column will still be frozen if it's in use. Setting the count to
+ * -1 will also disable the selection column.
+ * <p>
+ * The default value is 0.
+ *
+ * @param numberOfColumns
+ * the number of columns that should be frozen
+ *
+ * @throws IllegalArgumentException
+ * if the column count is < 0 or > the number of visible columns
+ */
+ public void setFrozenColumnCount(int numberOfColumns) {
+ if (numberOfColumns < -1 || numberOfColumns > columns.size()) {
+ throw new IllegalArgumentException(
+ "count must be between -1 and the current number of columns ("
+ + columns.size() + "): " + numberOfColumns);
+ }
+
+ getState().frozenColumnCount = numberOfColumns;
+ }
+
+ /**
+ * Gets the number of frozen columns in this grid. 0 means that no data
+ * columns will be frozen, but the built-in selection checkbox column will
+ * still be frozen if it's in use. -1 means that not even the selection
+ * column is frozen.
+ * <p>
+ * <em>NOTE:</em> this count includes {@link Column#isHidden() hidden
+ * columns} in the count.
+ *
+ * @see #setFrozenColumnCount(int)
+ *
+ * @return the number of frozen columns
+ */
+ public int getFrozenColumnCount() {
+ return getState(false).frozenColumnCount;
+ }
+
+ /**
+ * Scrolls to a certain item, using {@link ScrollDestination#ANY}.
+ * <p>
+ * If the item has visible details, its size will also be taken into
+ * account.
+ *
+ * @param itemId
+ * id of item to scroll to.
+ * @throws IllegalArgumentException
+ * if the provided id is not recognized by the data source.
+ */
+ public void scrollTo(Object itemId) throws IllegalArgumentException {
+ scrollTo(itemId, ScrollDestination.ANY);
+ }
+
+ /**
+ * Scrolls to a certain item, using user-specified scroll destination.
+ * <p>
+ * If the item has visible details, its size will also be taken into
+ * account.
+ *
+ * @param itemId
+ * id of item to scroll to.
+ * @param destination
+ * value specifying desired position of scrolled-to row.
+ * @throws IllegalArgumentException
+ * if the provided id is not recognized by the data source.
+ */
+ public void scrollTo(Object itemId, ScrollDestination destination)
+ throws IllegalArgumentException {
+
+ int row = datasource.indexOfId(itemId);
+
+ if (row == -1) {
+ throw new IllegalArgumentException(
+ "Item with specified ID does not exist in data source");
+ }
+
+ GridClientRpc clientRPC = getRpcProxy(GridClientRpc.class);
+ clientRPC.scrollToRow(row, destination);
+ }
+
+ /**
+ * Scrolls to the beginning of the first data row.
+ */
+ public void scrollToStart() {
+ GridClientRpc clientRPC = getRpcProxy(GridClientRpc.class);
+ clientRPC.scrollToStart();
+ }
+
+ /**
+ * Scrolls to the end of the last data row.
+ */
+ public void scrollToEnd() {
+ GridClientRpc clientRPC = getRpcProxy(GridClientRpc.class);
+ clientRPC.scrollToEnd();
+ }
+
+ /**
+ * Sets the number of rows that should be visible in Grid's body, while
+ * {@link #getHeightMode()} is {@link HeightMode#ROW}.
+ * <p>
+ * If Grid is currently not in {@link HeightMode#ROW}, the given value is
+ * remembered, and applied once the mode is applied.
+ *
+ * @param rows
+ * The height in terms of number of rows displayed in Grid's
+ * body. If Grid doesn't contain enough rows, white space is
+ * displayed instead. If <code>null</code> is given, then Grid's
+ * height is undefined
+ * @throws IllegalArgumentException
+ * if {@code rows} is zero or less
+ * @throws IllegalArgumentException
+ * if {@code rows} is {@link Double#isInfinite(double) infinite}
+ * @throws IllegalArgumentException
+ * if {@code rows} is {@link Double#isNaN(double) NaN}
+ */
+ public void setHeightByRows(double rows) {
+ if (rows <= 0.0d) {
+ throw new IllegalArgumentException(
+ "More than zero rows must be shown.");
+ } else if (Double.isInfinite(rows)) {
+ throw new IllegalArgumentException(
+ "Grid doesn't support infinite heights");
+ } else if (Double.isNaN(rows)) {
+ throw new IllegalArgumentException("NaN is not a valid row count");
+ }
+
+ getState().heightByRows = rows;
+ }
+
+ /**
+ * Gets the amount of rows in Grid's body that are shown, while
+ * {@link #getHeightMode()} is {@link HeightMode#ROW}.
+ *
+ * @return the amount of rows that are being shown in Grid's body
+ * @see #setHeightByRows(double)
+ */
+ public double getHeightByRows() {
+ return getState(false).heightByRows;
+ }
+
+ /**
+ * {@inheritDoc}
+ * <p>
+ * <em>Note:</em> This method will change the widget's size in the browser
+ * only if {@link #getHeightMode()} returns {@link HeightMode#CSS}.
+ *
+ * @see #setHeightMode(HeightMode)
+ */
+ @Override
+ public void setHeight(float height, Unit unit) {
+ super.setHeight(height, unit);
+ }
+
+ /**
+ * Defines the mode in which the Grid widget's height is calculated.
+ * <p>
+ * If {@link HeightMode#CSS} is given, Grid will respect the values given
+ * via a {@code setHeight}-method, and behave as a traditional Component.
+ * <p>
+ * If {@link HeightMode#ROW} is given, Grid will make sure that the body
+ * will display as many rows as {@link #getHeightByRows()} defines.
+ * <em>Note:</em> If headers/footers are inserted or removed, the widget
+ * will resize itself to still display the required amount of rows in its
+ * body. It also takes the horizontal scrollbar into account.
+ *
+ * @param heightMode
+ * the mode in to which Grid should be set
+ */
+ public void setHeightMode(HeightMode heightMode) {
+ /*
+ * This method is a workaround for the fact that Vaadin re-applies
+ * widget dimensions (height/width) on each state change event. The
+ * original design was to have setHeight and setHeightByRow be equals,
+ * and whichever was called the latest was considered in effect.
+ *
+ * But, because of Vaadin always calling setHeight on the widget, this
+ * approach doesn't work.
+ */
+
+ getState().heightMode = heightMode;
+ }
+
+ /**
+ * Returns the current {@link HeightMode} the Grid is in.
+ * <p>
+ * Defaults to {@link HeightMode#CSS}.
+ *
+ * @return the current HeightMode
+ */
+ public HeightMode getHeightMode() {
+ return getState(false).heightMode;
+ }
+
+ /* Selection related methods: */
+
+ /**
+ * Takes a new {@link SelectionModel} into use.
+ * <p>
+ * The SelectionModel that is previously in use will have all its items
+ * deselected.
+ * <p>
+ * If the given SelectionModel is already in use, this method does nothing.
+ *
+ * @param selectionModel
+ * the new SelectionModel to use
+ * @throws IllegalArgumentException
+ * if {@code selectionModel} is <code>null</code>
+ */
+ public void setSelectionModel(SelectionModel selectionModel)
+ throws IllegalArgumentException {
+ if (selectionModel == null) {
+ throw new IllegalArgumentException(
+ "Selection model may not be null");
+ }
+
+ if (this.selectionModel != selectionModel) {
+ // this.selectionModel is null on init
+ if (this.selectionModel != null) {
+ this.selectionModel.remove();
+ }
+
+ this.selectionModel = selectionModel;
+ selectionModel.setGrid(this);
+ }
+ }
+
+ /**
+ * Returns the currently used {@link SelectionModel}.
+ *
+ * @return the currently used SelectionModel
+ */
+ public SelectionModel getSelectionModel() {
+ return selectionModel;
+ }
+
+ /**
+ * Sets the Grid's selection mode.
+ * <p>
+ * Grid supports three selection modes: multiselect, single select and no
+ * selection, and this is a convenience method for choosing between one of
+ * them.
+ * <p>
+ * Technically, this method is a shortcut that can be used instead of
+ * calling {@code setSelectionModel} with a specific SelectionModel
+ * instance. Grid comes with three built-in SelectionModel classes, and the
+ * {@link SelectionMode} enum represents each of them.
+ * <p>
+ * Essentially, the two following method calls are equivalent:
+ * <p>
+ * <code><pre>
+ * grid.setSelectionMode(SelectionMode.MULTI);
+ * grid.setSelectionModel(new MultiSelectionMode());
+ * </pre></code>
+ *
+ *
+ * @param selectionMode
+ * the selection mode to switch to
+ * @return The {@link SelectionModel} instance that was taken into use
+ * @throws IllegalArgumentException
+ * if {@code selectionMode} is <code>null</code>
+ * @see SelectionModel
+ */
+ public SelectionModel setSelectionMode(final SelectionMode selectionMode)
+ throws IllegalArgumentException {
+ if (selectionMode == null) {
+ throw new IllegalArgumentException(
+ "selection mode may not be null");
+ }
+ final SelectionModel newSelectionModel = selectionMode.createModel();
+ setSelectionModel(newSelectionModel);
+ return newSelectionModel;
+ }
+
+ /**
+ * Checks whether an item is selected or not.
+ *
+ * @param itemId
+ * the item id to check for
+ * @return <code>true</code> iff the item is selected
+ */
+ // keep this javadoc in sync with SelectionModel.isSelected
+ public boolean isSelected(Object itemId) {
+ return selectionModel.isSelected(itemId);
+ }
+
+ /**
+ * Returns a collection of all the currently selected itemIds.
+ * <p>
+ * This method is a shorthand that delegates to the
+ * {@link #getSelectionModel() selection model}.
+ *
+ * @return a collection of all the currently selected itemIds
+ */
+ // keep this javadoc in sync with SelectionModel.getSelectedRows
+ public Collection<Object> getSelectedRows() {
+ return getSelectionModel().getSelectedRows();
+ }
+
+ /**
+ * Gets the item id of the currently selected item.
+ * <p>
+ * This method is a shorthand that delegates to the
+ * {@link #getSelectionModel() selection model}. Only
+ * {@link SelectionModel.Single} is supported.
+ *
+ * @return the item id of the currently selected item, or <code>null</code>
+ * if nothing is selected
+ * @throws IllegalStateException
+ * if the selection model does not implement
+ * {@code SelectionModel.Single}
+ */
+ // keep this javadoc in sync with SelectionModel.Single.getSelectedRow
+ public Object getSelectedRow() throws IllegalStateException {
+ if (selectionModel instanceof SelectionModel.Single) {
+ return ((SelectionModel.Single) selectionModel).getSelectedRow();
+ } else if (selectionModel instanceof SelectionModel.Multi) {
+ throw new IllegalStateException("Cannot get unique selected row: "
+ + "Grid is in multiselect mode "
+ + "(the current selection model is "
+ + selectionModel.getClass().getName() + ").");
+ } else if (selectionModel instanceof SelectionModel.None) {
+ throw new IllegalStateException(
+ "Cannot get selected row: " + "Grid selection is disabled "
+ + "(the current selection model is "
+ + selectionModel.getClass().getName() + ").");
+ } else {
+ throw new IllegalStateException("Cannot get selected row: "
+ + "Grid selection model does not implement "
+ + SelectionModel.Single.class.getName() + " or "
+ + SelectionModel.Multi.class.getName()
+ + "(the current model is "
+ + selectionModel.getClass().getName() + ").");
+ }
+ }
+
+ /**
+ * Marks an item as selected.
+ * <p>
+ * This method is a shorthand that delegates to the
+ * {@link #getSelectionModel() selection model}. Only
+ * {@link SelectionModel.Single} and {@link SelectionModel.Multi} are
+ * supported.
+ *
+ * @param itemId
+ * the itemId to mark as selected
+ * @return <code>true</code> if the selection state changed,
+ * <code>false</code> if the itemId already was selected
+ * @throws IllegalArgumentException
+ * if the {@code itemId} doesn't exist in the currently active
+ * Container
+ * @throws IllegalStateException
+ * if the selection was illegal. One such reason might be that
+ * the implementation already had an item selected, and that
+ * needs to be explicitly deselected before re-selecting
+ * something.
+ * @throws IllegalStateException
+ * if the selection model does not implement
+ * {@code SelectionModel.Single} or {@code SelectionModel.Multi}
+ */
+ // keep this javadoc in sync with SelectionModel.Single.select
+ public boolean select(Object itemId)
+ throws IllegalArgumentException, IllegalStateException {
+ if (selectionModel instanceof SelectionModel.Single) {
+ return ((SelectionModel.Single) selectionModel).select(itemId);
+ } else if (selectionModel instanceof SelectionModel.Multi) {
+ return ((SelectionModel.Multi) selectionModel).select(itemId);
+ } else if (selectionModel instanceof SelectionModel.None) {
+ throw new IllegalStateException("Cannot select row '" + itemId
+ + "': Grid selection is disabled "
+ + "(the current selection model is "
+ + selectionModel.getClass().getName() + ").");
+ } else {
+ throw new IllegalStateException("Cannot select row '" + itemId
+ + "': Grid selection model does not implement "
+ + SelectionModel.Single.class.getName() + " or "
+ + SelectionModel.Multi.class.getName()
+ + "(the current model is "
+ + selectionModel.getClass().getName() + ").");
+ }
+ }
+
+ /**
+ * Marks an item as unselected.
+ * <p>
+ * This method is a shorthand that delegates to the
+ * {@link #getSelectionModel() selection model}. Only
+ * {@link SelectionModel.Single} and {@link SelectionModel.Multi} are
+ * supported.
+ *
+ * @param itemId
+ * the itemId to remove from being selected
+ * @return <code>true</code> if the selection state changed,
+ * <code>false</code> if the itemId was already selected
+ * @throws IllegalArgumentException
+ * if the {@code itemId} doesn't exist in the currently active
+ * Container
+ * @throws IllegalStateException
+ * if the deselection was illegal. One such reason might be that
+ * the implementation requires one or more items to be selected
+ * at all times.
+ * @throws IllegalStateException
+ * if the selection model does not implement
+ * {@code SelectionModel.Single} or {code SelectionModel.Multi}
+ */
+ // keep this javadoc in sync with SelectionModel.Single.deselect
+ public boolean deselect(Object itemId) throws IllegalStateException {
+ if (selectionModel instanceof SelectionModel.Single) {
+ if (isSelected(itemId)) {
+ return ((SelectionModel.Single) selectionModel).select(null);
+ }
+ return false;
+ } else if (selectionModel instanceof SelectionModel.Multi) {
+ return ((SelectionModel.Multi) selectionModel).deselect(itemId);
+ } else if (selectionModel instanceof SelectionModel.None) {
+ throw new IllegalStateException("Cannot deselect row '" + itemId
+ + "': Grid selection is disabled "
+ + "(the current selection model is "
+ + selectionModel.getClass().getName() + ").");
+ } else {
+ throw new IllegalStateException("Cannot deselect row '" + itemId
+ + "': Grid selection model does not implement "
+ + SelectionModel.Single.class.getName() + " or "
+ + SelectionModel.Multi.class.getName()
+ + "(the current model is "
+ + selectionModel.getClass().getName() + ").");
+ }
+ }
+
+ /**
+ * Marks all items as unselected.
+ * <p>
+ * This method is a shorthand that delegates to the
+ * {@link #getSelectionModel() selection model}. Only
+ * {@link SelectionModel.Single} and {@link SelectionModel.Multi} are
+ * supported.
+ *
+ * @return <code>true</code> if the selection state changed,
+ * <code>false</code> if the itemId was already selected
+ * @throws IllegalStateException
+ * if the deselection was illegal. One such reason might be that
+ * the implementation requires one or more items to be selected
+ * at all times.
+ * @throws IllegalStateException
+ * if the selection model does not implement
+ * {@code SelectionModel.Single} or {code SelectionModel.Multi}
+ */
+ public boolean deselectAll() throws IllegalStateException {
+ if (selectionModel instanceof SelectionModel.Single) {
+ if (getSelectedRow() != null) {
+ return deselect(getSelectedRow());
+ }
+ return false;
+ } else if (selectionModel instanceof SelectionModel.Multi) {
+ return ((SelectionModel.Multi) selectionModel).deselectAll();
+ } else if (selectionModel instanceof SelectionModel.None) {
+ throw new IllegalStateException(
+ "Cannot deselect all rows" + ": Grid selection is disabled "
+ + "(the current selection model is "
+ + selectionModel.getClass().getName() + ").");
+ } else {
+ throw new IllegalStateException("Cannot deselect all rows:"
+ + " Grid selection model does not implement "
+ + SelectionModel.Single.class.getName() + " or "
+ + SelectionModel.Multi.class.getName()
+ + "(the current model is "
+ + selectionModel.getClass().getName() + ").");
+ }
+ }
+
+ /**
+ * Fires a selection change event.
+ * <p>
+ * <strong>Note:</strong> This is not a method that should be called by
+ * application logic. This method is publicly accessible only so that
+ * {@link SelectionModel SelectionModels} would be able to inform Grid of
+ * these events.
+ *
+ * @param newSelection
+ * the selection that was added by this event
+ * @param oldSelection
+ * the selection that was removed by this event
+ */
+ public void fireSelectionEvent(Collection<Object> oldSelection,
+ Collection<Object> newSelection) {
+ fireEvent(new SelectionEvent(this, oldSelection, newSelection));
+ }
+
+ @Override
+ public void addSelectionListener(SelectionListener listener) {
+ addListener(SelectionEvent.class, listener, SELECTION_CHANGE_METHOD);
+ }
+
+ @Override
+ public void removeSelectionListener(SelectionListener listener) {
+ removeListener(SelectionEvent.class, listener, SELECTION_CHANGE_METHOD);
+ }
+
+ private void fireColumnReorderEvent(boolean userOriginated) {
+ fireEvent(new ColumnReorderEvent(this, userOriginated));
+ }
+
+ /**
+ * Registers a new column reorder listener.
+ *
+ * @since 7.5.0
+ * @param listener
+ * the listener to register
+ */
+ public void addColumnReorderListener(ColumnReorderListener listener) {
+ addListener(ColumnReorderEvent.class, listener, COLUMN_REORDER_METHOD);
+ }
+
+ /**
+ * Removes a previously registered column reorder listener.
+ *
+ * @since 7.5.0
+ * @param listener
+ * the listener to remove
+ */
+ public void removeColumnReorderListener(ColumnReorderListener listener) {
+ removeListener(ColumnReorderEvent.class, listener,
+ COLUMN_REORDER_METHOD);
+ }
+
+ private void fireColumnResizeEvent(Column column, boolean userOriginated) {
+ fireEvent(new ColumnResizeEvent(this, column, userOriginated));
+ }
+
+ /**
+ * Registers a new column resize listener.
+ *
+ * @param listener
+ * the listener to register
+ */
+ public void addColumnResizeListener(ColumnResizeListener listener) {
+ addListener(ColumnResizeEvent.class, listener, COLUMN_RESIZE_METHOD);
+ }
+
+ /**
+ * Removes a previously registered column resize listener.
+ *
+ * @param listener
+ * the listener to remove
+ */
+ public void removeColumnResizeListener(ColumnResizeListener listener) {
+ removeListener(ColumnResizeEvent.class, listener, COLUMN_RESIZE_METHOD);
+ }
+
+ /**
+ * Gets the {@link KeyMapper } being used by the data source.
+ *
+ * @return the key mapper being used by the data source
+ */
+ KeyMapper<Object> getKeyMapper() {
+ return datasourceExtension.getKeyMapper();
+ }
+
+ /**
+ * Adds a renderer to this grid's connector hierarchy.
+ *
+ * @param renderer
+ * the renderer to add
+ */
+ void addRenderer(Renderer<?> renderer) {
+ addExtension(renderer);
+ }
+
+ /**
+ * Sets the current sort order using the fluid Sort API. Read the
+ * documentation for {@link Sort} for more information.
+ * <p>
+ * <em>Note:</em> Sorting by a property that has no column in Grid will hide
+ * all possible sorting indicators.
+ *
+ * @param s
+ * a sort instance
+ *
+ * @throws IllegalStateException
+ * if container is not sortable (does not implement
+ * Container.Sortable)
+ * @throws IllegalArgumentException
+ * if trying to sort by non-existing property
+ */
+ public void sort(Sort s) {
+ setSortOrder(s.build());
+ }
+
+ /**
+ * Sort this Grid in ascending order by a specified property.
+ * <p>
+ * <em>Note:</em> Sorting by a property that has no column in Grid will hide
+ * all possible sorting indicators.
+ *
+ * @param propertyId
+ * a property ID
+ *
+ * @throws IllegalStateException
+ * if container is not sortable (does not implement
+ * Container.Sortable)
+ * @throws IllegalArgumentException
+ * if trying to sort by non-existing property
+ */
+ public void sort(Object propertyId) {
+ sort(propertyId, SortDirection.ASCENDING);
+ }
+
+ /**
+ * Sort this Grid in user-specified {@link SortOrder} by a property.
+ * <p>
+ * <em>Note:</em> Sorting by a property that has no column in Grid will hide
+ * all possible sorting indicators.
+ *
+ * @param propertyId
+ * a property ID
+ * @param direction
+ * a sort order value (ascending/descending)
+ *
+ * @throws IllegalStateException
+ * if container is not sortable (does not implement
+ * Container.Sortable)
+ * @throws IllegalArgumentException
+ * if trying to sort by non-existing property
+ */
+ public void sort(Object propertyId, SortDirection direction) {
+ sort(Sort.by(propertyId, direction));
+ }
+
+ /**
+ * Clear the current sort order, and re-sort the grid.
+ */
+ public void clearSortOrder() {
+ sortOrder.clear();
+ sort(false);
+ }
+
+ /**
+ * Sets the sort order to use.
+ * <p>
+ * <em>Note:</em> Sorting by a property that has no column in Grid will hide
+ * all possible sorting indicators.
+ *
+ * @param order
+ * a sort order list.
+ *
+ * @throws IllegalStateException
+ * if container is not sortable (does not implement
+ * Container.Sortable)
+ * @throws IllegalArgumentException
+ * if order is null or trying to sort by non-existing property
+ */
+ public void setSortOrder(List<SortOrder> order) {
+ setSortOrder(order, false);
+ }
+
+ private void setSortOrder(List<SortOrder> order, boolean userOriginated)
+ throws IllegalStateException, IllegalArgumentException {
+ if (!(getContainerDataSource() instanceof Container.Sortable)) {
+ throw new IllegalStateException(
+ "Attached container is not sortable (does not implement Container.Sortable)");
+ }
+
+ if (order == null) {
+ throw new IllegalArgumentException("Order list may not be null!");
+ }
+
+ sortOrder.clear();
+
+ Collection<?> sortableProps = ((Container.Sortable) getContainerDataSource())
+ .getSortableContainerPropertyIds();
+
+ for (SortOrder o : order) {
+ if (!sortableProps.contains(o.getPropertyId())) {
+ throw new IllegalArgumentException("Property "
+ + o.getPropertyId()
+ + " does not exist or is not sortable in the current container");
+ }
+ }
+
+ sortOrder.addAll(order);
+ sort(userOriginated);
+ }
+
+ /**
+ * Get the current sort order list.
+ *
+ * @return a sort order list
+ */
+ public List<SortOrder> getSortOrder() {
+ return Collections.unmodifiableList(sortOrder);
+ }
+
+ /**
+ * Apply sorting to data source.
+ */
+ private void sort(boolean userOriginated) {
+
+ Container c = getContainerDataSource();
+ if (c instanceof Container.Sortable) {
+ Container.Sortable cs = (Container.Sortable) c;
+
+ final int items = sortOrder.size();
+ Object[] propertyIds = new Object[items];
+ boolean[] directions = new boolean[items];
+
+ SortDirection[] stateDirs = new SortDirection[items];
+
+ for (int i = 0; i < items; ++i) {
+ SortOrder order = sortOrder.get(i);
+
+ stateDirs[i] = order.getDirection();
+ propertyIds[i] = order.getPropertyId();
+ switch (order.getDirection()) {
+ case ASCENDING:
+ directions[i] = true;
+ break;
+ case DESCENDING:
+ directions[i] = false;
+ break;
+ default:
+ throw new IllegalArgumentException("getDirection() of "
+ + order + " returned an unexpected value");
+ }
+ }
+
+ cs.sort(propertyIds, directions);
+
+ if (columns.keySet().containsAll(Arrays.asList(propertyIds))) {
+ String[] columnKeys = new String[items];
+ for (int i = 0; i < items; ++i) {
+ columnKeys[i] = this.columnKeys.key(propertyIds[i]);
+ }
+ getState().sortColumns = columnKeys;
+ getState(false).sortDirs = stateDirs;
+ } else {
+ // Not all sorted properties are in Grid. Remove any indicators.
+ getState().sortColumns = new String[] {};
+ getState(false).sortDirs = new SortDirection[] {};
+ }
+ fireEvent(new SortEvent(this, new ArrayList<>(sortOrder),
+ userOriginated));
+ } else {
+ throw new IllegalStateException(
+ "Container is not sortable (does not implement Container.Sortable)");
+ }
+ }
+
+ /**
+ * Adds a sort order change listener that gets notified when the sort order
+ * changes.
+ *
+ * @param listener
+ * the sort order change listener to add
+ */
+ @Override
+ public void addSortListener(SortListener listener) {
+ addListener(SortEvent.class, listener, SORT_ORDER_CHANGE_METHOD);
+ }
+
+ /**
+ * Removes a sort order change listener previously added using
+ * {@link #addSortListener(SortListener)}.
+ *
+ * @param listener
+ * the sort order change listener to remove
+ */
+ @Override
+ public void removeSortListener(SortListener listener) {
+ removeListener(SortEvent.class, listener, SORT_ORDER_CHANGE_METHOD);
+ }
+
+ /* Grid Headers */
+
+ /**
+ * Returns the header section of this grid. The default header contains a
+ * single row displaying the column captions.
+ *
+ * @return the header
+ */
+ protected Header getHeader() {
+ return header;
+ }
+
+ /**
+ * Gets the header row at given index.
+ *
+ * @param rowIndex
+ * 0 based index for row. Counted from top to bottom
+ * @return header row at given index
+ * @throws IllegalArgumentException
+ * if no row exists at given index
+ */
+ public HeaderRow getHeaderRow(int rowIndex) {
+ return header.getRow(rowIndex);
+ }
+
+ /**
+ * Inserts a new row at the given position to the header section. Shifts the
+ * row currently at that position and any subsequent rows down (adds one to
+ * their indices).
+ *
+ * @param index
+ * the position at which to insert the row
+ * @return the new row
+ *
+ * @throws IllegalArgumentException
+ * if the index is less than 0 or greater than row count
+ * @see #appendHeaderRow()
+ * @see #prependHeaderRow()
+ * @see #removeHeaderRow(HeaderRow)
+ * @see #removeHeaderRow(int)
+ */
+ public HeaderRow addHeaderRowAt(int index) {
+ return header.addRowAt(index);
+ }
+
+ /**
+ * Adds a new row at the bottom of the header section.
+ *
+ * @return the new row
+ * @see #prependHeaderRow()
+ * @see #addHeaderRowAt(int)
+ * @see #removeHeaderRow(HeaderRow)
+ * @see #removeHeaderRow(int)
+ */
+ public HeaderRow appendHeaderRow() {
+ return header.appendRow();
+ }
+
+ /**
+ * Returns the current default row of the header section. The default row is
+ * a special header row providing a user interface for sorting columns.
+ * Setting a header text for column updates cells in the default header.
+ *
+ * @return the default row or null if no default row set
+ */
+ public HeaderRow getDefaultHeaderRow() {
+ return header.getDefaultRow();
+ }
+
+ /**
+ * Gets the row count for the header section.
+ *
+ * @return row count
+ */
+ public int getHeaderRowCount() {
+ return header.getRowCount();
+ }
+
+ /**
+ * Adds a new row at the top of the header section.
+ *
+ * @return the new row
+ * @see #appendHeaderRow()
+ * @see #addHeaderRowAt(int)
+ * @see #removeHeaderRow(HeaderRow)
+ * @see #removeHeaderRow(int)
+ */
+ public HeaderRow prependHeaderRow() {
+ return header.prependRow();
+ }
+
+ /**
+ * Removes the given row from the header section.
+ *
+ * @param row
+ * the row to be removed
+ *
+ * @throws IllegalArgumentException
+ * if the row does not exist in this section
+ * @see #removeHeaderRow(int)
+ * @see #addHeaderRowAt(int)
+ * @see #appendHeaderRow()
+ * @see #prependHeaderRow()
+ */
+ public void removeHeaderRow(HeaderRow row) {
+ header.removeRow(row);
+ }
+
+ /**
+ * Removes the row at the given position from the header section.
+ *
+ * @param rowIndex
+ * the position of the row
+ *
+ * @throws IllegalArgumentException
+ * if no row exists at given index
+ * @see #removeHeaderRow(HeaderRow)
+ * @see #addHeaderRowAt(int)
+ * @see #appendHeaderRow()
+ * @see #prependHeaderRow()
+ */
+ public void removeHeaderRow(int rowIndex) {
+ header.removeRow(rowIndex);
+ }
+
+ /**
+ * Sets the default row of the header. The default row is a special header
+ * row providing a user interface for sorting columns.
+ *
+ * @param row
+ * the new default row, or null for no default row
+ *
+ * @throws IllegalArgumentException
+ * header does not contain the row
+ */
+ public void setDefaultHeaderRow(HeaderRow row) {
+ header.setDefaultRow(row);
+ }
+
+ /**
+ * Sets the visibility of the header section.
+ *
+ * @param visible
+ * true to show header section, false to hide
+ */
+ public void setHeaderVisible(boolean visible) {
+ header.setVisible(visible);
+ }
+
+ /**
+ * Returns the visibility of the header section.
+ *
+ * @return true if visible, false otherwise.
+ */
+ public boolean isHeaderVisible() {
+ return header.isVisible();
+ }
+
+ /* Grid Footers */
+
+ /**
+ * Returns the footer section of this grid. The default header contains a
+ * single row displaying the column captions.
+ *
+ * @return the footer
+ */
+ protected Footer getFooter() {
+ return footer;
+ }
+
+ /**
+ * Gets the footer row at given index.
+ *
+ * @param rowIndex
+ * 0 based index for row. Counted from top to bottom
+ * @return footer row at given index
+ * @throws IllegalArgumentException
+ * if no row exists at given index
+ */
+ public FooterRow getFooterRow(int rowIndex) {
+ return footer.getRow(rowIndex);
+ }
+
+ /**
+ * Inserts a new row at the given position to the footer section. Shifts the
+ * row currently at that position and any subsequent rows down (adds one to
+ * their indices).
+ *
+ * @param index
+ * the position at which to insert the row
+ * @return the new row
+ *
+ * @throws IllegalArgumentException
+ * if the index is less than 0 or greater than row count
+ * @see #appendFooterRow()
+ * @see #prependFooterRow()
+ * @see #removeFooterRow(FooterRow)
+ * @see #removeFooterRow(int)
+ */
+ public FooterRow addFooterRowAt(int index) {
+ return footer.addRowAt(index);
+ }
+
+ /**
+ * Adds a new row at the bottom of the footer section.
+ *
+ * @return the new row
+ * @see #prependFooterRow()
+ * @see #addFooterRowAt(int)
+ * @see #removeFooterRow(FooterRow)
+ * @see #removeFooterRow(int)
+ */
+ public FooterRow appendFooterRow() {
+ return footer.appendRow();
+ }
+
+ /**
+ * Gets the row count for the footer.
+ *
+ * @return row count
+ */
+ public int getFooterRowCount() {
+ return footer.getRowCount();
+ }
+
+ /**
+ * Adds a new row at the top of the footer section.
+ *
+ * @return the new row
+ * @see #appendFooterRow()
+ * @see #addFooterRowAt(int)
+ * @see #removeFooterRow(FooterRow)
+ * @see #removeFooterRow(int)
+ */
+ public FooterRow prependFooterRow() {
+ return footer.prependRow();
+ }
+
+ /**
+ * Removes the given row from the footer section.
+ *
+ * @param row
+ * the row to be removed
+ *
+ * @throws IllegalArgumentException
+ * if the row does not exist in this section
+ * @see #removeFooterRow(int)
+ * @see #addFooterRowAt(int)
+ * @see #appendFooterRow()
+ * @see #prependFooterRow()
+ */
+ public void removeFooterRow(FooterRow row) {
+ footer.removeRow(row);
+ }
+
+ /**
+ * Removes the row at the given position from the footer section.
+ *
+ * @param rowIndex
+ * the position of the row
+ *
+ * @throws IllegalArgumentException
+ * if no row exists at given index
+ * @see #removeFooterRow(FooterRow)
+ * @see #addFooterRowAt(int)
+ * @see #appendFooterRow()
+ * @see #prependFooterRow()
+ */
+ public void removeFooterRow(int rowIndex) {
+ footer.removeRow(rowIndex);
+ }
+
+ /**
+ * Sets the visibility of the footer section.
+ *
+ * @param visible
+ * true to show footer section, false to hide
+ */
+ public void setFooterVisible(boolean visible) {
+ footer.setVisible(visible);
+ }
+
+ /**
+ * Returns the visibility of the footer section.
+ *
+ * @return true if visible, false otherwise.
+ */
+ public boolean isFooterVisible() {
+ return footer.isVisible();
+ }
+
+ private void addComponent(Component c) {
+ extensionComponents.add(c);
+ c.setParent(this);
+ markAsDirty();
+ }
+
+ private void removeComponent(Component c) {
+ extensionComponents.remove(c);
+ c.setParent(null);
+ markAsDirty();
+ }
+
+ @Override
+ public Iterator<Component> iterator() {
+ // This is a hash set to avoid adding header/footer components inside
+ // merged cells multiple times
+ LinkedHashSet<Component> componentList = new LinkedHashSet<>();
+
+ Header header = getHeader();
+ for (int i = 0; i < header.getRowCount(); ++i) {
+ HeaderRow row = header.getRow(i);
+ for (Object propId : columns.keySet()) {
+ HeaderCell cell = row.getCell(propId);
+ if (cell.getCellState().type == GridStaticCellType.WIDGET) {
+ componentList.add(cell.getComponent());
+ }
+ }
+ }
+
+ Footer footer = getFooter();
+ for (int i = 0; i < footer.getRowCount(); ++i) {
+ FooterRow row = footer.getRow(i);
+ for (Object propId : columns.keySet()) {
+ FooterCell cell = row.getCell(propId);
+ if (cell.getCellState().type == GridStaticCellType.WIDGET) {
+ componentList.add(cell.getComponent());
+ }
+ }
+ }
+
+ componentList.addAll(getEditorFields());
+
+ componentList.addAll(extensionComponents);
+
+ return componentList.iterator();
+ }
+
+ @Override
+ public boolean isRendered(Component childComponent) {
+ if (getEditorFields().contains(childComponent)) {
+ // Only render editor fields if the editor is open
+ return isEditorActive();
+ } else {
+ // TODO Header and footer components should also only be rendered if
+ // the header/footer is visible
+ return true;
+ }
+ }
+
+ EditorClientRpc getEditorRpc() {
+ return getRpcProxy(EditorClientRpc.class);
+ }
+
+ /**
+ * Sets the {@code CellDescriptionGenerator} instance for generating
+ * optional descriptions (tooltips) for individual Grid cells. If a
+ * {@link RowDescriptionGenerator} is also set, the row description it
+ * generates is displayed for cells for which {@code generator} returns
+ * null.
+ *
+ * @param generator
+ * the description generator to use or {@code null} to remove a
+ * previously set generator if any
+ *
+ * @see #setRowDescriptionGenerator(RowDescriptionGenerator)
+ *
+ * @since 7.6
+ */
+ public void setCellDescriptionGenerator(
+ CellDescriptionGenerator generator) {
+ cellDescriptionGenerator = generator;
+ getState().hasDescriptions = (generator != null
+ || rowDescriptionGenerator != null);
+ datasourceExtension.refreshCache();
+ }
+
+ /**
+ * Returns the {@code CellDescriptionGenerator} instance used to generate
+ * descriptions (tooltips) for Grid cells.
+ *
+ * @return the description generator or {@code null} if no generator is set
+ *
+ * @since 7.6
+ */
+ public CellDescriptionGenerator getCellDescriptionGenerator() {
+ return cellDescriptionGenerator;
+ }
+
+ /**
+ * Sets the {@code RowDescriptionGenerator} instance for generating optional
+ * descriptions (tooltips) for Grid rows. If a
+ * {@link CellDescriptionGenerator} is also set, the row description
+ * generated by {@code generator} is used for cells for which the cell
+ * description generator returns null.
+ *
+ *
+ * @param generator
+ * the description generator to use or {@code null} to remove a
+ * previously set generator if any
+ *
+ * @see #setCellDescriptionGenerator(CellDescriptionGenerator)
+ *
+ * @since 7.6
+ */
+ public void setRowDescriptionGenerator(RowDescriptionGenerator generator) {
+ rowDescriptionGenerator = generator;
+ getState().hasDescriptions = (generator != null
+ || cellDescriptionGenerator != null);
+ datasourceExtension.refreshCache();
+ }
+
+ /**
+ * Returns the {@code RowDescriptionGenerator} instance used to generate
+ * descriptions (tooltips) for Grid rows
+ *
+ * @return the description generator or {@code} null if no generator is set
+ *
+ * @since 7.6
+ */
+ public RowDescriptionGenerator getRowDescriptionGenerator() {
+ return rowDescriptionGenerator;
+ }
+
+ /**
+ * Sets the style generator that is used for generating styles for cells
+ *
+ * @param cellStyleGenerator
+ * the cell style generator to set, or <code>null</code> to
+ * remove a previously set generator
+ */
+ public void setCellStyleGenerator(CellStyleGenerator cellStyleGenerator) {
+ this.cellStyleGenerator = cellStyleGenerator;
+ datasourceExtension.refreshCache();
+ }
+
+ /**
+ * Gets the style generator that is used for generating styles for cells
+ *
+ * @return the cell style generator, or <code>null</code> if no generator is
+ * set
+ */
+ public CellStyleGenerator getCellStyleGenerator() {
+ return cellStyleGenerator;
+ }
+
+ /**
+ * Sets the style generator that is used for generating styles for rows
+ *
+ * @param rowStyleGenerator
+ * the row style generator to set, or <code>null</code> to remove
+ * a previously set generator
+ */
+ public void setRowStyleGenerator(RowStyleGenerator rowStyleGenerator) {
+ this.rowStyleGenerator = rowStyleGenerator;
+ datasourceExtension.refreshCache();
+ }
+
+ /**
+ * Gets the style generator that is used for generating styles for rows
+ *
+ * @return the row style generator, or <code>null</code> if no generator is
+ * set
+ */
+ public RowStyleGenerator getRowStyleGenerator() {
+ return rowStyleGenerator;
+ }
+
+ /**
+ * Adds a row to the underlying container. The order of the parameters
+ * should match the current visible column order.
+ * <p>
+ * Please note that it's generally only safe to use this method during
+ * initialization. After Grid has been initialized and the visible column
+ * order might have been changed, it's better to instead add items directly
+ * to the underlying container and use {@link Item#getItemProperty(Object)}
+ * to make sure each value is assigned to the intended property.
+ *
+ * @param values
+ * the cell values of the new row, in the same order as the
+ * visible column order, not <code>null</code>.
+ * @return the item id of the new row
+ * @throws IllegalArgumentException
+ * if values is null
+ * @throws IllegalArgumentException
+ * if its length does not match the number of visible columns
+ * @throws IllegalArgumentException
+ * if a parameter value is not an instance of the corresponding
+ * property type
+ * @throws UnsupportedOperationException
+ * if the container does not support adding new items
+ */
+ public Object addRow(Object... values) {
+ if (values == null) {
+ throw new IllegalArgumentException("Values cannot be null");
+ }
+
+ Indexed dataSource = getContainerDataSource();
+ List<String> columnOrder = getState(false).columnOrder;
+
+ if (values.length != columnOrder.size()) {
+ throw new IllegalArgumentException(
+ "There are " + columnOrder.size() + " visible columns, but "
+ + values.length + " cell values were provided.");
+ }
+
+ // First verify all parameter types
+ for (int i = 0; i < columnOrder.size(); i++) {
+ Object propertyId = getPropertyIdByColumnId(columnOrder.get(i));
+
+ Class<?> propertyType = dataSource.getType(propertyId);
+ if (values[i] != null && !propertyType.isInstance(values[i])) {
+ throw new IllegalArgumentException("Parameter " + i + "("
+ + values[i] + ") is not an instance of "
+ + propertyType.getCanonicalName());
+ }
+ }
+
+ Object itemId = dataSource.addItem();
+ try {
+ Item item = dataSource.getItem(itemId);
+ for (int i = 0; i < columnOrder.size(); i++) {
+ Object propertyId = getPropertyIdByColumnId(columnOrder.get(i));
+ Property<Object> property = item.getItemProperty(propertyId);
+ property.setValue(values[i]);
+ }
+ } catch (RuntimeException e) {
+ try {
+ dataSource.removeItem(itemId);
+ } catch (Exception e2) {
+ getLogger().log(Level.SEVERE,
+ "Error recovering from exception in addRow", e);
+ }
+ throw e;
+ }
+
+ return itemId;
+ }
+
+ private static Logger getLogger() {
+ return Logger.getLogger(LegacyGrid.class.getName());
+ }
+
+ /**
+ * Sets whether or not the item editor UI is enabled for this grid. When the
+ * editor is enabled, the user can open it by double-clicking a row or
+ * hitting enter when a row is focused. The editor can also be opened
+ * programmatically using the {@link #editItem(Object)} method.
+ *
+ * @param isEnabled
+ * <code>true</code> to enable the feature, <code>false</code>
+ * otherwise
+ * @throws IllegalStateException
+ * if an item is currently being edited
+ *
+ * @see #getEditedItemId()
+ */
+ public void setEditorEnabled(boolean isEnabled)
+ throws IllegalStateException {
+ if (isEditorActive()) {
+ throw new IllegalStateException(
+ "Cannot disable the editor while an item ("
+ + getEditedItemId() + ") is being edited");
+ }
+ if (isEditorEnabled() != isEnabled) {
+ getState().editorEnabled = isEnabled;
+ }
+ }
+
+ /**
+ * Checks whether the item editor UI is enabled for this grid.
+ *
+ * @return <code>true</code> iff the editor is enabled for this grid
+ *
+ * @see #setEditorEnabled(boolean)
+ * @see #getEditedItemId()
+ */
+ public boolean isEditorEnabled() {
+ return getState(false).editorEnabled;
+ }
+
+ /**
+ * Gets the id of the item that is currently being edited.
+ *
+ * @return the id of the item that is currently being edited, or
+ * <code>null</code> if no item is being edited at the moment
+ */
+ public Object getEditedItemId() {
+ return editedItemId;
+ }
+
+ /**
+ * Gets the field group that is backing the item editor of this grid.
+ *
+ * @return the backing field group
+ */
+ public FieldGroup getEditorFieldGroup() {
+ return editorFieldGroup;
+ }
+
+ /**
+ * Sets the field group that is backing the item editor of this grid.
+ *
+ * @param fieldGroup
+ * the backing field group
+ *
+ * @throws IllegalStateException
+ * if the editor is currently active
+ */
+ public void setEditorFieldGroup(FieldGroup fieldGroup) {
+ if (isEditorActive()) {
+ throw new IllegalStateException(
+ "Cannot change field group while an item ("
+ + getEditedItemId() + ") is being edited");
+ }
+ editorFieldGroup = fieldGroup;
+ }
+
+ /**
+ * Returns whether an item is currently being edited in the editor.
+ *
+ * @return true iff the editor is open
+ */
+ public boolean isEditorActive() {
+ return editorActive;
+ }
+
+ private void checkColumnExists(Object propertyId) {
+ if (getColumn(propertyId) == null) {
+ throw new IllegalArgumentException(
+ "There is no column with the property id " + propertyId);
+ }
+ }
+
+ private LegacyField<?> getEditorField(Object propertyId) {
+ checkColumnExists(propertyId);
+
+ if (!getColumn(propertyId).isEditable()) {
+ return null;
+ }
+
+ LegacyField<?> editor = editorFieldGroup.getField(propertyId);
+
+ try {
+ if (editor == null) {
+ editor = editorFieldGroup.buildAndBind(propertyId);
+ }
+ } finally {
+ if (editor == null) {
+ editor = editorFieldGroup.getField(propertyId);
+ }
+
+ if (editor != null && editor.getParent() != LegacyGrid.this) {
+ assert editor.getParent() == null;
+ editor.setParent(this);
+ }
+ }
+ return editor;
+ }
+
+ /**
+ * Opens the editor interface for the provided item. Scrolls the Grid to
+ * bring the item to view if it is not already visible.
+ *
+ * Note that any cell content rendered by a WidgetRenderer will not be
+ * visible in the editor row.
+ *
+ * @param itemId
+ * the id of the item to edit
+ * @throws IllegalStateException
+ * if the editor is not enabled or already editing an item in
+ * buffered mode
+ * @throws IllegalArgumentException
+ * if the {@code itemId} is not in the backing container
+ * @see #setEditorEnabled(boolean)
+ */
+ public void editItem(Object itemId)
+ throws IllegalStateException, IllegalArgumentException {
+ if (!isEditorEnabled()) {
+ throw new IllegalStateException("Item editor is not enabled");
+ } else if (isEditorBuffered() && editedItemId != null) {
+ throw new IllegalStateException("Editing item " + itemId
+ + " failed. Item editor is already editing item "
+ + editedItemId);
+ } else if (!getContainerDataSource().containsId(itemId)) {
+ throw new IllegalArgumentException("Item with id " + itemId
+ + " not found in current container");
+ }
+ editedItemId = itemId;
+ getEditorRpc().bind(getContainerDataSource().indexOfId(itemId));
+ }
+
+ protected void doEditItem() {
+ Item item = getContainerDataSource().getItem(editedItemId);
+
+ editorFieldGroup.setItemDataSource(item);
+
+ for (Column column : getColumns()) {
+ column.getState().editorConnector = getEditorField(
+ column.getPropertyId());
+ }
+
+ editorActive = true;
+ // Must ensure that all fields, recursively, are sent to the client
+ // This is needed because the fields are hidden using isRendered
+ for (LegacyField<?> f : getEditorFields()) {
+ f.markAsDirtyRecursive();
+ }
+
+ if (datasource instanceof ItemSetChangeNotifier) {
+ ((ItemSetChangeNotifier) datasource)
+ .addItemSetChangeListener(editorClosingItemSetListener);
+ }
+ }
+
+ private void setEditorField(Object propertyId, LegacyField<?> field) {
+ checkColumnExists(propertyId);
+
+ LegacyField<?> oldField = editorFieldGroup.getField(propertyId);
+ if (oldField != null) {
+ editorFieldGroup.unbind(oldField);
+ oldField.setParent(null);
+ }
+
+ if (field != null) {
+ field.setParent(this);
+ editorFieldGroup.bind(field, propertyId);
+ }
+ }
+
+ /**
+ * Saves all changes done to the bound fields.
+ * <p>
+ * <em>Note:</em> This is a pass-through call to the backing field group.
+ *
+ * @throws CommitException
+ * If the commit was aborted
+ *
+ * @see FieldGroup#commit()
+ */
+ public void saveEditor() throws CommitException {
+ editorFieldGroup.commit();
+ }
+
+ /**
+ * Cancels the currently active edit if any. Hides the editor and discards
+ * possible unsaved changes in the editor fields.
+ */
+ public void cancelEditor() {
+ if (isEditorActive()) {
+ getEditorRpc()
+ .cancel(getContainerDataSource().indexOfId(editedItemId));
+ doCancelEditor();
+ }
+ }
+
+ protected void doCancelEditor() {
+ editedItemId = null;
+ editorActive = false;
+ editorFieldGroup.discard();
+ editorFieldGroup.setItemDataSource(null);
+
+ if (datasource instanceof ItemSetChangeNotifier) {
+ ((ItemSetChangeNotifier) datasource)
+ .removeItemSetChangeListener(editorClosingItemSetListener);
+ }
+
+ // Mark Grid as dirty so the client side gets to know that the editors
+ // are no longer attached
+ markAsDirty();
+ }
+
+ void resetEditor() {
+ if (isEditorActive()) {
+ /*
+ * Simply force cancel the editing; throwing here would just make
+ * Grid.setContainerDataSource semantics more complicated.
+ */
+ cancelEditor();
+ }
+ for (LegacyField<?> editor : getEditorFields()) {
+ editor.setParent(null);
+ }
+
+ editedItemId = null;
+ editorActive = false;
+ editorFieldGroup = new CustomFieldGroup();
+ }
+
+ /**
+ * Gets a collection of all fields bound to the item editor of this grid.
+ * <p>
+ * When {@link #editItem(Object) editItem} is called, fields are
+ * automatically created and bound to any unbound properties.
+ *
+ * @return a collection of all the fields bound to the item editor
+ */
+ Collection<LegacyField<?>> getEditorFields() {
+ Collection<LegacyField<?>> fields = editorFieldGroup.getFields();
+ assert allAttached(fields);
+ return fields;
+ }
+
+ private boolean allAttached(Collection<? extends Component> components) {
+ for (Component component : components) {
+ if (component.getParent() != this) {
+ return false;
+ }
+ }
+ return true;
+ }
+
+ /**
+ * Sets the field factory for the {@link FieldGroup}. The field factory is
+ * only used when {@link FieldGroup} creates a new field.
+ * <p>
+ * <em>Note:</em> This is a pass-through call to the backing field group.
+ *
+ * @param fieldFactory
+ * The field factory to use
+ */
+ public void setEditorFieldFactory(FieldGroupFieldFactory fieldFactory) {
+ editorFieldGroup.setFieldFactory(fieldFactory);
+ }
+
+ /**
+ * Sets the error handler for the editor.
+ *
+ * The error handler is called whenever there is an exception in the editor.
+ *
+ * @param editorErrorHandler
+ * The editor error handler to use
+ * @throws IllegalArgumentException
+ * if the error handler is null
+ */
+ public void setEditorErrorHandler(EditorErrorHandler editorErrorHandler)
+ throws IllegalArgumentException {
+ if (editorErrorHandler == null) {
+ throw new IllegalArgumentException(
+ "The error handler cannot be null");
+ }
+ this.editorErrorHandler = editorErrorHandler;
+ }
+
+ /**
+ * Gets the error handler used for the editor
+ *
+ * @see #setErrorHandler(com.vaadin.server.ErrorHandler)
+ * @return the editor error handler, never null
+ */
+ public EditorErrorHandler getEditorErrorHandler() {
+ return editorErrorHandler;
+ }
+
+ /**
+ * Gets the field factory for the {@link FieldGroup}. The field factory is
+ * only used when {@link FieldGroup} creates a new field.
+ * <p>
+ * <em>Note:</em> This is a pass-through call to the backing field group.
+ *
+ * @return The field factory in use
+ */
+ public FieldGroupFieldFactory getEditorFieldFactory() {
+ return editorFieldGroup.getFieldFactory();
+ }
+
+ /**
+ * Sets the caption on the save button in the Grid editor.
+ *
+ * @param saveCaption
+ * the caption to set
+ * @throws IllegalArgumentException
+ * if {@code saveCaption} is {@code null}
+ */
+ public void setEditorSaveCaption(String saveCaption)
+ throws IllegalArgumentException {
+ if (saveCaption == null) {
+ throw new IllegalArgumentException("Save caption cannot be null");
+ }
+ getState().editorSaveCaption = saveCaption;
+ }
+
+ /**
+ * Gets the current caption of the save button in the Grid editor.
+ *
+ * @return the current caption of the save button
+ */
+ public String getEditorSaveCaption() {
+ return getState(false).editorSaveCaption;
+ }
+
+ /**
+ * Sets the caption on the cancel button in the Grid editor.
+ *
+ * @param cancelCaption
+ * the caption to set
+ * @throws IllegalArgumentException
+ * if {@code cancelCaption} is {@code null}
+ */
+ public void setEditorCancelCaption(String cancelCaption)
+ throws IllegalArgumentException {
+ if (cancelCaption == null) {
+ throw new IllegalArgumentException("Cancel caption cannot be null");
+ }
+ getState().editorCancelCaption = cancelCaption;
+ }
+
+ /**
+ * Gets the current caption of the cancel button in the Grid editor.
+ *
+ * @return the current caption of the cancel button
+ */
+ public String getEditorCancelCaption() {
+ return getState(false).editorCancelCaption;
+ }
+
+ /**
+ * Sets the buffered editor mode. The default mode is buffered (
+ * <code>true</code>).
+ *
+ * @since 7.6
+ * @param editorBuffered
+ * <code>true</code> to enable buffered editor,
+ * <code>false</code> to disable it
+ * @throws IllegalStateException
+ * If editor is active while attempting to change the buffered
+ * mode.
+ */
+ public void setEditorBuffered(boolean editorBuffered)
+ throws IllegalStateException {
+ if (isEditorActive()) {
+ throw new IllegalStateException(
+ "Can't change editor unbuffered mode while editor is active.");
+ }
+ getState().editorBuffered = editorBuffered;
+ editorFieldGroup.setBuffered(editorBuffered);
+ }
+
+ /**
+ * Gets the buffered editor mode.
+ *
+ * @since 7.6
+ * @return <code>true</code> if buffered editor is enabled,
+ * <code>false</code> otherwise
+ */
+ public boolean isEditorBuffered() {
+ return getState(false).editorBuffered;
+ }
+
+ @Override
+ public void addItemClickListener(ItemClickListener listener) {
+ addListener(GridConstants.ITEM_CLICK_EVENT_ID, ItemClickEvent.class,
+ listener, ItemClickEvent.ITEM_CLICK_METHOD);
+ }
+
+ @Override
+ @Deprecated
+ public void addListener(ItemClickListener listener) {
+ addItemClickListener(listener);
+ }
+
+ @Override
+ public void removeItemClickListener(ItemClickListener listener) {
+ removeListener(GridConstants.ITEM_CLICK_EVENT_ID, ItemClickEvent.class,
+ listener);
+ }
+
+ @Override
+ @Deprecated
+ public void removeListener(ItemClickListener listener) {
+ removeItemClickListener(listener);
+ }
+
+ /**
+ * Requests that the column widths should be recalculated.
+ * <p>
+ * In most cases Grid will know when column widths need to be recalculated
+ * but this method can be used to force recalculation in situations when
+ * grid does not recalculate automatically.
+ *
+ * @since 7.4.1
+ */
+ public void recalculateColumnWidths() {
+ getRpcProxy(GridClientRpc.class).recalculateColumnWidths();
+ }
+
+ /**
+ * Registers a new column visibility change listener
+ *
+ * @since 7.5.0
+ * @param listener
+ * the listener to register
+ */
+ public void addColumnVisibilityChangeListener(
+ ColumnVisibilityChangeListener listener) {
+ addListener(ColumnVisibilityChangeEvent.class, listener,
+ COLUMN_VISIBILITY_METHOD);
+ }
+
+ /**
+ * Removes a previously registered column visibility change listener
+ *
+ * @since 7.5.0
+ * @param listener
+ * the listener to remove
+ */
+ public void removeColumnVisibilityChangeListener(
+ ColumnVisibilityChangeListener listener) {
+ removeListener(ColumnVisibilityChangeEvent.class, listener,
+ COLUMN_VISIBILITY_METHOD);
+ }
+
+ private void fireColumnVisibilityChangeEvent(Column column, boolean hidden,
+ boolean isUserOriginated) {
+ fireEvent(new ColumnVisibilityChangeEvent(this, column, hidden,
+ isUserOriginated));
+ }
+
+ /**
+ * Sets a new details generator for row details.
+ * <p>
+ * The currently opened row details will be re-rendered.
+ *
+ * @since 7.5.0
+ * @param detailsGenerator
+ * the details generator to set
+ * @throws IllegalArgumentException
+ * if detailsGenerator is <code>null</code>;
+ */
+ public void setDetailsGenerator(DetailsGenerator detailsGenerator)
+ throws IllegalArgumentException {
+ detailComponentManager.setDetailsGenerator(detailsGenerator);
+ }
+
+ /**
+ * Gets the current details generator for row details.
+ *
+ * @since 7.5.0
+ * @return the detailsGenerator the current details generator
+ */
+ public DetailsGenerator getDetailsGenerator() {
+ return detailComponentManager.getDetailsGenerator();
+ }
+
+ /**
+ * Shows or hides the details for a specific item.
+ *
+ * @since 7.5.0
+ * @param itemId
+ * the id of the item for which to set details visibility
+ * @param visible
+ * <code>true</code> to show the details, or <code>false</code>
+ * to hide them
+ */
+ public void setDetailsVisible(Object itemId, boolean visible) {
+ detailComponentManager.setDetailsVisible(itemId, visible);
+ }
+
+ /**
+ * Checks whether details are visible for the given item.
+ *
+ * @since 7.5.0
+ * @param itemId
+ * the id of the item for which to check details visibility
+ * @return <code>true</code> iff the details are visible
+ */
+ public boolean isDetailsVisible(Object itemId) {
+ return detailComponentManager.isDetailsVisible(itemId);
+ }
+
+ private static SelectionMode getDefaultSelectionMode() {
+ return SelectionMode.SINGLE;
+ }
+
+ @Override
+ public void readDesign(Element design, DesignContext context) {
+ super.readDesign(design, context);
+
+ Attributes attrs = design.attributes();
+ if (attrs.hasKey("editable")) {
+ setEditorEnabled(DesignAttributeHandler.readAttribute("editable",
+ attrs, boolean.class));
+ }
+ if (attrs.hasKey("rows")) {
+ setHeightByRows(DesignAttributeHandler.readAttribute("rows", attrs,
+ double.class));
+ setHeightMode(HeightMode.ROW);
+ }
+ if (attrs.hasKey("selection-mode")) {
+ setSelectionMode(DesignAttributeHandler.readAttribute(
+ "selection-mode", attrs, SelectionMode.class));
+ }
+
+ if (design.children().size() > 0) {
+ if (design.children().size() > 1
+ || !design.child(0).tagName().equals("table")) {
+ throw new DesignException(
+ "Grid needs to have a table element as its only child");
+ }
+ Element table = design.child(0);
+
+ Elements colgroups = table.getElementsByTag("colgroup");
+ if (colgroups.size() != 1) {
+ throw new DesignException(
+ "Table element in declarative Grid needs to have a"
+ + " colgroup defining the columns used in Grid");
+ }
+
+ int i = 0;
+ for (Element col : colgroups.get(0).getElementsByTag("col")) {
+ String propertyId = DesignAttributeHandler.readAttribute(
+ "property-id", col.attributes(), "property-" + i,
+ String.class);
+ addColumn(propertyId, String.class).readDesign(col, context);
+ ++i;
+ }
+
+ for (Element child : table.children()) {
+ if (child.tagName().equals("thead")) {
+ header.readDesign(child, context);
+ } else if (child.tagName().equals("tbody")) {
+ for (Element row : child.children()) {
+ Elements cells = row.children();
+ Object[] data = new String[cells.size()];
+ for (int c = 0; c < cells.size(); ++c) {
+ data[c] = cells.get(c).html();
+ }
+ addRow(data);
+ }
+
+ // Since inline data is used, set HTML renderer for columns
+ for (Column c : getColumns()) {
+ c.setRenderer(new HtmlRenderer());
+ }
+ } else if (child.tagName().equals("tfoot")) {
+ footer.readDesign(child, context);
+ }
+ }
+ }
+
+ // Read frozen columns after columns are read.
+ if (attrs.hasKey("frozen-columns")) {
+ setFrozenColumnCount(DesignAttributeHandler
+ .readAttribute("frozen-columns", attrs, int.class));
+ }
+ }
+
+ @Override
+ public void writeDesign(Element design, DesignContext context) {
+ super.writeDesign(design, context);
+
+ Attributes attrs = design.attributes();
+ LegacyGrid def = context.getDefaultInstance(this);
+
+ DesignAttributeHandler.writeAttribute("editable", attrs,
+ isEditorEnabled(), def.isEditorEnabled(), boolean.class);
+
+ DesignAttributeHandler.writeAttribute("frozen-columns", attrs,
+ getFrozenColumnCount(), def.getFrozenColumnCount(), int.class);
+
+ if (getHeightMode() == HeightMode.ROW) {
+ DesignAttributeHandler.writeAttribute("rows", attrs,
+ getHeightByRows(), def.getHeightByRows(), double.class);
+ }
+
+ SelectionMode selectionMode = null;
+
+ if (selectionModel.getClass().equals(SingleSelectionModel.class)) {
+ selectionMode = SelectionMode.SINGLE;
+ } else if (selectionModel.getClass()
+ .equals(MultiSelectionModel.class)) {
+ selectionMode = SelectionMode.MULTI;
+ } else if (selectionModel.getClass().equals(NoSelectionModel.class)) {
+ selectionMode = SelectionMode.NONE;
+ }
+
+ assert selectionMode != null : "Unexpected selection model "
+ + selectionModel.getClass().getName();
+
+ DesignAttributeHandler.writeAttribute("selection-mode", attrs,
+ selectionMode, getDefaultSelectionMode(), SelectionMode.class);
+
+ if (columns.isEmpty()) {
+ // Empty grid. Structure not needed.
+ return;
+ }
+
+ // Do structure.
+ Element tableElement = design.appendElement("table");
+ Element colGroup = tableElement.appendElement("colgroup");
+
+ List<Column> columnOrder = getColumns();
+ for (int i = 0; i < columnOrder.size(); ++i) {
+ Column column = columnOrder.get(i);
+ Element colElement = colGroup.appendElement("col");
+ column.writeDesign(colElement, context);
+ }
+
+ // Always write thead. Reads correctly when there no header rows
+ header.writeDesign(tableElement.appendElement("thead"), context);
+
+ if (context.shouldWriteData(this)) {
+ Element bodyElement = tableElement.appendElement("tbody");
+ for (Object itemId : datasource.getItemIds()) {
+ Element tableRow = bodyElement.appendElement("tr");
+ for (Column c : getColumns()) {
+ Object value = datasource.getItem(itemId)
+ .getItemProperty(c.getPropertyId()).getValue();
+ tableRow.appendElement("td")
+ .append((value != null ? DesignFormatter
+ .encodeForTextNode(value.toString()) : ""));
+ }
+ }
+ }
+
+ if (footer.getRowCount() > 0) {
+ footer.writeDesign(tableElement.appendElement("tfoot"), context);
+ }
+ }
+
+ @Override
+ protected Collection<String> getCustomAttributes() {
+ Collection<String> result = super.getCustomAttributes();
+ result.add("editor-enabled");
+ result.add("editable");
+ result.add("frozen-column-count");
+ result.add("frozen-columns");
+ result.add("height-by-rows");
+ result.add("rows");
+ result.add("selection-mode");
+ result.add("header-visible");
+ result.add("footer-visible");
+ result.add("editor-error-handler");
+ result.add("height-mode");
+
+ return result;
+ }
+}
diff --git a/compatibility-server/src/main/java/com/vaadin/ui/renderers/AbstractJavaScriptRenderer.java b/compatibility-server/src/main/java/com/vaadin/ui/renderers/AbstractJavaScriptRenderer.java
new file mode 100644
index 0000000000..63c6f5aeaf
--- /dev/null
+++ b/compatibility-server/src/main/java/com/vaadin/ui/renderers/AbstractJavaScriptRenderer.java
@@ -0,0 +1,175 @@
+/*
+ * Copyright 2000-2016 Vaadin Ltd.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not
+ * use this file except in compliance with the License. You may obtain a copy of
+ * the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+package com.vaadin.ui.renderers;
+
+import com.vaadin.server.AbstractJavaScriptExtension;
+import com.vaadin.server.JavaScriptCallbackHelper;
+import com.vaadin.server.JsonCodec;
+import com.vaadin.shared.JavaScriptExtensionState;
+import com.vaadin.shared.communication.ServerRpc;
+import com.vaadin.ui.LegacyGrid.AbstractRenderer;
+import com.vaadin.ui.JavaScriptFunction;
+
+import elemental.json.Json;
+import elemental.json.JsonValue;
+
+/**
+ * Base class for Renderers with all client-side logic implemented using
+ * JavaScript.
+ * <p>
+ * When a new JavaScript renderer is initialized in the browser, the framework
+ * will look for a globally defined JavaScript function that will initialize the
+ * renderer. The name of the initialization function is formed by replacing .
+ * with _ in the name of the server-side class. If no such function is defined,
+ * each super class is used in turn until a match is found. The framework will
+ * thus first attempt with <code>com_example_MyRenderer</code> for the
+ * server-side
+ * <code>com.example.MyRenderer extends AbstractJavaScriptRenderer</code> class.
+ * If MyRenderer instead extends <code>com.example.SuperRenderer</code> , then
+ * <code>com_example_SuperRenderer</code> will also be attempted if
+ * <code>com_example_MyRenderer</code> has not been defined.
+ * <p>
+ *
+ * In addition to the general JavaScript extension functionality explained in
+ * {@link AbstractJavaScriptExtension}, this class also provides some
+ * functionality specific for renderers.
+ * <p>
+ * The initialization function will be called with <code>this</code> pointing to
+ * a connector wrapper object providing integration to Vaadin. Please note that
+ * in JavaScript, <code>this</code> is not necessarily defined inside callback
+ * functions and it might therefore be necessary to assign the reference to a
+ * separate variable, e.g. <code>var self = this;</code>. In addition to the
+ * extension functions described for {@link AbstractJavaScriptExtension}, the
+ * connector wrapper object also provides this function:
+ * <ul>
+ * <li><code>getRowKey(rowIndex)</code> - Gets a unique identifier for the row
+ * at the given index. This identifier can be used on the server to retrieve the
+ * corresponding ItemId using {@link #getItemId(String)}.</li>
+ * </ul>
+ * The connector wrapper also supports these special functions that can be
+ * implemented by the connector:
+ * <ul>
+ * <li><code>render(cell, data)</code> - Callback for rendering the given data
+ * into the given cell. The structure of cell and data are described in separate
+ * sections below. The renderer is required to implement this function.
+ * Corresponds to
+ * {@link com.vaadin.client.renderers.Renderer#render(com.vaadin.client.widget.grid.RendererCellReference, Object)}
+ * .</li>
+ * <li><code>init(cell)</code> - Prepares a cell for rendering. Corresponds to
+ * {@link com.vaadin.client.renderers.ComplexRenderer#init(com.vaadin.client.widget.grid.RendererCellReference)}
+ * .</li>
+ * <li><code>destory(cell)</code> - Allows the renderer to release resources
+ * allocate for a cell that will no longer be used. Corresponds to
+ * {@link com.vaadin.client.renderers.ComplexRenderer#destroy(com.vaadin.client.widget.grid.RendererCellReference)}
+ * .</li>
+ * <li><code>onActivate(cell)</code> - Called when the cell is activated by the
+ * user e.g. by double clicking on the cell or pressing enter with the cell
+ * focused. Corresponds to
+ * {@link com.vaadin.client.renderers.ComplexRenderer#onActivate(com.vaadin.client.widget.grid.CellReference)}
+ * .</li>
+ * <li><code>getConsumedEvents()</code> - Returns a JavaScript array of event
+ * names that should cause onBrowserEvent to be invoked whenever an event is
+ * fired for a cell managed by this renderer. Corresponds to
+ * {@link com.vaadin.client.renderers.ComplexRenderer#getConsumedEvents()}.</li>
+ * <li><code>onBrowserEvent(cell, event)</code> - Called by Grid when an event
+ * of a type returned by getConsumedEvents is fired for a cell managed by this
+ * renderer. Corresponds to
+ * {@link com.vaadin.client.renderers.ComplexRenderer#onBrowserEvent(com.vaadin.client.widget.grid.CellReference, com.google.gwt.dom.client.NativeEvent)}
+ * .</li>
+ * </ul>
+ *
+ * <p>
+ * The cell object passed to functions defined by the renderer has these
+ * properties:
+ * <ul>
+ * <li><code>element</code> - The DOM element corresponding to this cell.
+ * Readonly.</li>
+ * <li><code>rowIndex</code> - The current index of the row of this cell.
+ * Readonly.</li>
+ * <li><code>columnIndex</code> - The current index of the column of this cell.
+ * Readonly.</li>
+ * <li><code>colSpan</code> - The number of columns spanned by this cell. Only
+ * supported in the object passed to the <code>render</code> function - other
+ * functions should not use the property. Readable and writable.
+ * </ul>
+ *
+ * @author Vaadin Ltd
+ * @since 7.4
+ */
+public abstract class AbstractJavaScriptRenderer<T>
+ extends AbstractRenderer<T> {
+ private JavaScriptCallbackHelper callbackHelper = new JavaScriptCallbackHelper(
+ this);
+
+ protected AbstractJavaScriptRenderer(Class<T> presentationType,
+ String nullRepresentation) {
+ super(presentationType, nullRepresentation);
+ }
+
+ protected AbstractJavaScriptRenderer(Class<T> presentationType) {
+ super(presentationType, null);
+ }
+
+ @Override
+ protected <R extends ServerRpc> void registerRpc(R implementation,
+ Class<R> rpcInterfaceType) {
+ super.registerRpc(implementation, rpcInterfaceType);
+ callbackHelper.registerRpc(rpcInterfaceType);
+ }
+
+ /**
+ * Register a {@link JavaScriptFunction} that can be called from the
+ * JavaScript using the provided name. A JavaScript function with the
+ * provided name will be added to the connector wrapper object (initially
+ * available as <code>this</code>). Calling that JavaScript function will
+ * cause the call method in the registered {@link JavaScriptFunction} to be
+ * invoked with the same arguments.
+ *
+ * @param functionName
+ * the name that should be used for client-side callback
+ * @param function
+ * the {@link JavaScriptFunction} object that will be invoked
+ * when the JavaScript function is called
+ */
+ protected void addFunction(String functionName,
+ JavaScriptFunction function) {
+ callbackHelper.registerCallback(functionName, function);
+ }
+
+ /**
+ * Invoke a named function that the connector JavaScript has added to the
+ * JavaScript connector wrapper object. The arguments can be any boxed
+ * primitive type, String, {@link JsonValue} or arrays of any other
+ * supported type. Complex types (e.g. List, Set, Map, Connector or any
+ * JavaBean type) must be explicitly serialized to a {@link JsonValue}
+ * before sending. This can be done either with
+ * {@link JsonCodec#encode(Object, JsonValue, java.lang.reflect.Type, com.vaadin.ui.ConnectorTracker)}
+ * or using the factory methods in {@link Json}.
+ *
+ * @param name
+ * the name of the function
+ * @param arguments
+ * function arguments
+ */
+ protected void callFunction(String name, Object... arguments) {
+ callbackHelper.invokeCallback(name, arguments);
+ }
+
+ @Override
+ protected JavaScriptExtensionState getState() {
+ return (JavaScriptExtensionState) super.getState();
+ }
+}
diff --git a/compatibility-server/src/main/java/com/vaadin/ui/renderers/ButtonRenderer.java b/compatibility-server/src/main/java/com/vaadin/ui/renderers/ButtonRenderer.java
new file mode 100644
index 0000000000..7a87f8f4c1
--- /dev/null
+++ b/compatibility-server/src/main/java/com/vaadin/ui/renderers/ButtonRenderer.java
@@ -0,0 +1,76 @@
+/*
+ * Copyright 2000-2016 Vaadin Ltd.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not
+ * use this file except in compliance with the License. You may obtain a copy of
+ * the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+package com.vaadin.ui.renderers;
+
+import elemental.json.JsonValue;
+
+/**
+ * A Renderer that displays a button with a textual caption. The value of the
+ * corresponding property is used as the caption. Click listeners can be added
+ * to the renderer, invoked when any of the rendered buttons is clicked.
+ *
+ * @since 7.4
+ * @author Vaadin Ltd
+ */
+public class ButtonRenderer extends ClickableRenderer<String> {
+
+ /**
+ * Creates a new button renderer.
+ *
+ * @param nullRepresentation
+ * the textual representation of {@code null} value
+ */
+ public ButtonRenderer(String nullRepresentation) {
+ super(String.class, nullRepresentation);
+ }
+
+ /**
+ * Creates a new button renderer and adds the given click listener to it.
+ *
+ * @param listener
+ * the click listener to register
+ * @param nullRepresentation
+ * the textual representation of {@code null} value
+ */
+ public ButtonRenderer(RendererClickListener listener,
+ String nullRepresentation) {
+ this(nullRepresentation);
+ addClickListener(listener);
+ }
+
+ /**
+ * Creates a new button renderer.
+ */
+ public ButtonRenderer() {
+ this("");
+ }
+
+ /**
+ * Creates a new button renderer and adds the given click listener to it.
+ *
+ * @param listener
+ * the click listener to register
+ */
+ public ButtonRenderer(RendererClickListener listener) {
+ this(listener, "");
+ }
+
+ @Override
+ public String getNullRepresentation() {
+ return super.getNullRepresentation();
+ }
+
+}
diff --git a/compatibility-server/src/main/java/com/vaadin/ui/renderers/ClickableRenderer.java b/compatibility-server/src/main/java/com/vaadin/ui/renderers/ClickableRenderer.java
new file mode 100644
index 0000000000..d8fcdc3b4e
--- /dev/null
+++ b/compatibility-server/src/main/java/com/vaadin/ui/renderers/ClickableRenderer.java
@@ -0,0 +1,143 @@
+/*
+ * Copyright 2000-2016 Vaadin Ltd.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not
+ * use this file except in compliance with the License. You may obtain a copy of
+ * the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+package com.vaadin.ui.renderers;
+
+import java.lang.reflect.Method;
+
+import com.vaadin.event.ConnectorEventListener;
+import com.vaadin.event.MouseEvents.ClickEvent;
+import com.vaadin.shared.MouseEventDetails;
+import com.vaadin.shared.ui.grid.renderers.RendererClickRpc;
+import com.vaadin.ui.LegacyGrid;
+import com.vaadin.ui.LegacyGrid.AbstractRenderer;
+import com.vaadin.ui.LegacyGrid.Column;
+import com.vaadin.util.ReflectTools;
+
+/**
+ * An abstract superclass for Renderers that render clickable items. Click
+ * listeners can be added to a renderer to be notified when any of the rendered
+ * items is clicked.
+ *
+ * @param <T>
+ * the type presented by the renderer
+ *
+ * @since 7.4
+ * @author Vaadin Ltd
+ */
+public class ClickableRenderer<T> extends AbstractRenderer<T> {
+
+ /**
+ * An interface for listening to {@link RendererClickEvent renderer click
+ * events}.
+ *
+ * @see {@link ButtonRenderer#addClickListener(RendererClickListener)}
+ */
+ public interface RendererClickListener extends ConnectorEventListener {
+
+ static final Method CLICK_METHOD = ReflectTools.findMethod(
+ RendererClickListener.class, "click", RendererClickEvent.class);
+
+ /**
+ * Called when a rendered button is clicked.
+ *
+ * @param event
+ * the event representing the click
+ */
+ void click(RendererClickEvent event);
+ }
+
+ /**
+ * An event fired when a button rendered by a ButtonRenderer is clicked.
+ */
+ public static class RendererClickEvent extends ClickEvent {
+
+ private Object itemId;
+ private Column column;
+
+ protected RendererClickEvent(LegacyGrid source, Object itemId, Column column,
+ MouseEventDetails mouseEventDetails) {
+ super(source, mouseEventDetails);
+ this.itemId = itemId;
+ this.column = column;
+ }
+
+ /**
+ * Returns the item ID of the row where the click event originated.
+ *
+ * @return the item ID of the clicked row
+ */
+ public Object getItemId() {
+ return itemId;
+ }
+
+ /**
+ * Returns the {@link Column} where the click event originated.
+ *
+ * @return the column of the click event
+ */
+ public Column getColumn() {
+ return column;
+ }
+
+ /**
+ * Returns the property ID where the click event originated.
+ *
+ * @return the property ID of the clicked cell
+ */
+ public Object getPropertyId() {
+ return column.getPropertyId();
+ }
+ }
+
+ protected ClickableRenderer(Class<T> presentationType) {
+ this(presentationType, null);
+ }
+
+ protected ClickableRenderer(Class<T> presentationType,
+ String nullRepresentation) {
+ super(presentationType, nullRepresentation);
+ registerRpc(new RendererClickRpc() {
+ @Override
+ public void click(String rowKey, String columnId,
+ MouseEventDetails mouseDetails) {
+ fireEvent(new RendererClickEvent(getParentGrid(),
+ getItemId(rowKey), getColumn(columnId), mouseDetails));
+ }
+ });
+ }
+
+ /**
+ * Adds a click listener to this button renderer. The listener is invoked
+ * every time one of the buttons rendered by this renderer is clicked.
+ *
+ * @param listener
+ * the click listener to be added
+ */
+ public void addClickListener(RendererClickListener listener) {
+ addListener(RendererClickEvent.class, listener,
+ RendererClickListener.CLICK_METHOD);
+ }
+
+ /**
+ * Removes the given click listener from this renderer.
+ *
+ * @param listener
+ * the click listener to be removed
+ */
+ public void removeClickListener(RendererClickListener listener) {
+ removeListener(RendererClickEvent.class, listener);
+ }
+}
diff --git a/compatibility-server/src/main/java/com/vaadin/ui/renderers/DateRenderer.java b/compatibility-server/src/main/java/com/vaadin/ui/renderers/DateRenderer.java
new file mode 100644
index 0000000000..eea18e7445
--- /dev/null
+++ b/compatibility-server/src/main/java/com/vaadin/ui/renderers/DateRenderer.java
@@ -0,0 +1,240 @@
+/*
+ * Copyright 2000-2016 Vaadin Ltd.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not
+ * use this file except in compliance with the License. You may obtain a copy of
+ * the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+package com.vaadin.ui.renderers;
+
+import java.text.DateFormat;
+import java.util.Date;
+import java.util.Locale;
+
+import com.vaadin.ui.LegacyGrid.AbstractRenderer;
+
+import elemental.json.JsonValue;
+
+/**
+ * A renderer for presenting date values.
+ *
+ * @since 7.4
+ * @author Vaadin Ltd
+ */
+public class DateRenderer extends AbstractRenderer<Date> {
+ private final Locale locale;
+ private final String formatString;
+ private final DateFormat dateFormat;
+
+ /**
+ * Creates a new date renderer.
+ * <p>
+ * The renderer is configured to render with the {@link Date#toString()}
+ * representation for the default locale.
+ */
+ public DateRenderer() {
+ this(Locale.getDefault(), "");
+ }
+
+ /**
+ * Creates a new date renderer.
+ * <p>
+ * The renderer is configured to render with the {@link Date#toString()}
+ * representation for the given locale.
+ *
+ * @param locale
+ * the locale in which to present dates
+ * @throws IllegalArgumentException
+ * if {@code locale} is {@code null}
+ */
+ public DateRenderer(Locale locale) throws IllegalArgumentException {
+ this("%s", locale, "");
+ }
+
+ /**
+ * Creates a new date renderer.
+ * <p>
+ * The renderer is configured to render with the {@link Date#toString()}
+ * representation for the given locale.
+ *
+ * @param locale
+ * the locale in which to present dates
+ * @param nullRepresentation
+ * the textual representation of {@code null} value
+ * @throws IllegalArgumentException
+ * if {@code locale} is {@code null}
+ */
+ public DateRenderer(Locale locale, String nullRepresentation)
+ throws IllegalArgumentException {
+ this("%s", locale, nullRepresentation);
+ }
+
+ /**
+ * Creates a new date renderer.
+ * <p>
+ * The renderer is configured to render with the given string format, as
+ * displayed in the default locale.
+ *
+ * @param formatString
+ * the format string with which to format the date
+ * @throws IllegalArgumentException
+ * if {@code formatString} is {@code null}
+ * @see <a href=
+ * "http://docs.oracle.com/javase/7/docs/api/java/util/Formatter.html#syntax">Format
+ * String Syntax</a>
+ */
+ public DateRenderer(String formatString) throws IllegalArgumentException {
+ this(formatString, "");
+ }
+
+ /**
+ * Creates a new date renderer.
+ * <p>
+ * The renderer is configured to render with the given string format, as
+ * displayed in the default locale.
+ *
+ * @param formatString
+ * the format string with which to format the date
+ * @param nullRepresentation
+ * the textual representation of {@code null} value
+ * @throws IllegalArgumentException
+ * if {@code formatString} is {@code null}
+ * @see <a href=
+ * "http://docs.oracle.com/javase/7/docs/api/java/util/Formatter.html#syntax">Format
+ * String Syntax</a>
+ */
+ public DateRenderer(String formatString, String nullRepresentation)
+ throws IllegalArgumentException {
+ this(formatString, Locale.getDefault(), nullRepresentation);
+ }
+
+ /**
+ * Creates a new date renderer.
+ * <p>
+ * The renderer is configured to render with the given string format, as
+ * displayed in the given locale.
+ *
+ * @param formatString
+ * the format string to format the date with
+ * @param locale
+ * the locale to use
+ * @throws IllegalArgumentException
+ * if either argument is {@code null}
+ * @see <a href=
+ * "http://docs.oracle.com/javase/7/docs/api/java/util/Formatter.html#syntax">Format
+ * String Syntax</a>
+ */
+ public DateRenderer(String formatString, Locale locale)
+ throws IllegalArgumentException {
+ this(formatString, locale, "");
+ }
+
+ /**
+ * Creates a new date renderer.
+ * <p>
+ * The renderer is configured to render with the given string format, as
+ * displayed in the given locale.
+ *
+ * @param formatString
+ * the format string to format the date with
+ * @param locale
+ * the locale to use
+ * @param nullRepresentation
+ * the textual representation of {@code null} value
+ * @throws IllegalArgumentException
+ * if either argument is {@code null}
+ * @see <a href=
+ * "http://docs.oracle.com/javase/7/docs/api/java/util/Formatter.html#syntax">Format
+ * String Syntax</a>
+ */
+ public DateRenderer(String formatString, Locale locale,
+ String nullRepresentation) throws IllegalArgumentException {
+ super(Date.class, nullRepresentation);
+
+ if (formatString == null) {
+ throw new IllegalArgumentException("format string may not be null");
+ }
+
+ if (locale == null) {
+ throw new IllegalArgumentException("locale may not be null");
+ }
+
+ this.locale = locale;
+ this.formatString = formatString;
+ dateFormat = null;
+ }
+
+ /**
+ * Creates a new date renderer.
+ * <p>
+ * The renderer is configured to render with he given date format.
+ *
+ * @param dateFormat
+ * the date format to use when rendering dates
+ * @throws IllegalArgumentException
+ * if {@code dateFormat} is {@code null}
+ */
+ public DateRenderer(DateFormat dateFormat) throws IllegalArgumentException {
+ this(dateFormat, "");
+ }
+
+ /**
+ * Creates a new date renderer.
+ * <p>
+ * The renderer is configured to render with he given date format.
+ *
+ * @param dateFormat
+ * the date format to use when rendering dates
+ * @throws IllegalArgumentException
+ * if {@code dateFormat} is {@code null}
+ */
+ public DateRenderer(DateFormat dateFormat, String nullRepresentation)
+ throws IllegalArgumentException {
+ super(Date.class, nullRepresentation);
+ if (dateFormat == null) {
+ throw new IllegalArgumentException("date format may not be null");
+ }
+
+ locale = null;
+ formatString = null;
+ this.dateFormat = dateFormat;
+ }
+
+ @Override
+ public String getNullRepresentation() {
+ return super.getNullRepresentation();
+ }
+
+ @Override
+ public JsonValue encode(Date value) {
+ String dateString;
+ if (value == null) {
+ dateString = getNullRepresentation();
+ } else if (dateFormat != null) {
+ dateString = dateFormat.format(value);
+ } else {
+ dateString = String.format(locale, formatString, value);
+ }
+ return encode(dateString, String.class);
+ }
+
+ @Override
+ public String toString() {
+ final String fieldInfo;
+ if (dateFormat != null) {
+ fieldInfo = "dateFormat: " + dateFormat.toString();
+ } else {
+ fieldInfo = "locale: " + locale + ", formatString: " + formatString;
+ }
+
+ return String.format("%s [%s]", getClass().getSimpleName(), fieldInfo);
+ }
+}
diff --git a/compatibility-server/src/main/java/com/vaadin/ui/renderers/HtmlRenderer.java b/compatibility-server/src/main/java/com/vaadin/ui/renderers/HtmlRenderer.java
new file mode 100644
index 0000000000..c197f7415a
--- /dev/null
+++ b/compatibility-server/src/main/java/com/vaadin/ui/renderers/HtmlRenderer.java
@@ -0,0 +1,49 @@
+/*
+ * Copyright 2000-2016 Vaadin Ltd.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not
+ * use this file except in compliance with the License. You may obtain a copy of
+ * the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+package com.vaadin.ui.renderers;
+
+import com.vaadin.ui.LegacyGrid.AbstractRenderer;
+import elemental.json.JsonValue;
+
+/**
+ * A renderer for presenting HTML content.
+ *
+ * @author Vaadin Ltd
+ * @since 7.4
+ */
+public class HtmlRenderer extends AbstractRenderer<String> {
+ /**
+ * Creates a new HTML renderer.
+ *
+ * @param nullRepresentation
+ * the html representation of {@code null} value
+ */
+ public HtmlRenderer(String nullRepresentation) {
+ super(String.class, nullRepresentation);
+ }
+
+ /**
+ * Creates a new HTML renderer.
+ */
+ public HtmlRenderer() {
+ this("");
+ }
+
+ @Override
+ public String getNullRepresentation() {
+ return super.getNullRepresentation();
+ }
+}
diff --git a/compatibility-server/src/main/java/com/vaadin/ui/renderers/ImageRenderer.java b/compatibility-server/src/main/java/com/vaadin/ui/renderers/ImageRenderer.java
new file mode 100644
index 0000000000..56e319b47d
--- /dev/null
+++ b/compatibility-server/src/main/java/com/vaadin/ui/renderers/ImageRenderer.java
@@ -0,0 +1,68 @@
+/*
+ * Copyright 2000-2016 Vaadin Ltd.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not
+ * use this file except in compliance with the License. You may obtain a copy of
+ * the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+package com.vaadin.ui.renderers;
+
+import com.vaadin.server.ExternalResource;
+import com.vaadin.server.Resource;
+import com.vaadin.server.ResourceReference;
+import com.vaadin.server.ThemeResource;
+import com.vaadin.shared.communication.URLReference;
+
+import elemental.json.JsonValue;
+
+/**
+ * A renderer for presenting images.
+ * <p>
+ * The image for each rendered cell is read from a Resource-typed property in
+ * the data source. Only {@link ExternalResource}s and {@link ThemeResource}s
+ * are currently supported.
+ *
+ * @since 7.4
+ * @author Vaadin Ltd
+ */
+public class ImageRenderer extends ClickableRenderer<Resource> {
+
+ /**
+ * Creates a new image renderer.
+ */
+ public ImageRenderer() {
+ super(Resource.class, null);
+ }
+
+ /**
+ * Creates a new image renderer and adds the given click listener to it.
+ *
+ * @param listener
+ * the click listener to register
+ */
+ public ImageRenderer(RendererClickListener listener) {
+ this();
+ addClickListener(listener);
+ }
+
+ @Override
+ public JsonValue encode(Resource resource) {
+ if (!(resource == null || resource instanceof ExternalResource
+ || resource instanceof ThemeResource)) {
+ throw new IllegalArgumentException(
+ "ImageRenderer only supports ExternalResource and ThemeResource ("
+ + resource.getClass().getSimpleName() + " given)");
+ }
+
+ return encode(ResourceReference.create(resource, this, null),
+ URLReference.class);
+ }
+}
diff --git a/compatibility-server/src/main/java/com/vaadin/ui/renderers/NumberRenderer.java b/compatibility-server/src/main/java/com/vaadin/ui/renderers/NumberRenderer.java
new file mode 100644
index 0000000000..e54eecc6ef
--- /dev/null
+++ b/compatibility-server/src/main/java/com/vaadin/ui/renderers/NumberRenderer.java
@@ -0,0 +1,207 @@
+/*
+ * Copyright 2000-2016 Vaadin Ltd.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not
+ * use this file except in compliance with the License. You may obtain a copy of
+ * the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+package com.vaadin.ui.renderers;
+
+import java.text.NumberFormat;
+import java.util.Locale;
+
+import com.vaadin.ui.LegacyGrid.AbstractRenderer;
+
+import elemental.json.JsonValue;
+
+/**
+ * A renderer for presenting number values.
+ *
+ * @since 7.4
+ * @author Vaadin Ltd
+ */
+public class NumberRenderer extends AbstractRenderer<Number> {
+ private final Locale locale;
+ private final NumberFormat numberFormat;
+ private final String formatString;
+
+ /**
+ * Creates a new number renderer.
+ * <p/>
+ * The renderer is configured to render with the number's natural string
+ * representation in the default locale.
+ */
+ public NumberRenderer() {
+ this(Locale.getDefault());
+ }
+
+ /**
+ * Creates a new number renderer.
+ * <p/>
+ * The renderer is configured to render the number as defined with the given
+ * number format.
+ *
+ * @param numberFormat
+ * the number format with which to display numbers
+ * @throws IllegalArgumentException
+ * if {@code numberFormat} is {@code null}
+ */
+ public NumberRenderer(NumberFormat numberFormat) {
+ this(numberFormat, "");
+ }
+
+ /**
+ * Creates a new number renderer.
+ * <p/>
+ * The renderer is configured to render the number as defined with the given
+ * number format.
+ *
+ * @param numberFormat
+ * the number format with which to display numbers
+ * @param nullRepresentation
+ * the textual representation of {@code null} value
+ * @throws IllegalArgumentException
+ * if {@code numberFormat} is {@code null}
+ */
+ public NumberRenderer(NumberFormat numberFormat, String nullRepresentation)
+ throws IllegalArgumentException {
+ super(Number.class, nullRepresentation);
+
+ if (numberFormat == null) {
+ throw new IllegalArgumentException("Number format may not be null");
+ }
+
+ locale = null;
+ this.numberFormat = numberFormat;
+ formatString = null;
+ }
+
+ /**
+ * Creates a new number renderer.
+ * <p/>
+ * The renderer is configured to render with the number's natural string
+ * representation in the given locale.
+ *
+ * @param locale
+ * the locale in which to display numbers
+ * @throws IllegalArgumentException
+ * if {@code locale} is {@code null}
+ */
+ public NumberRenderer(Locale locale) throws IllegalArgumentException {
+ this("%s", locale);
+ }
+
+ /**
+ * Creates a new number renderer.
+ * <p/>
+ * The renderer is configured to render with the number's natural string
+ * representation in the given locale.
+ *
+ * @param formatString
+ * the format string with which to format the number
+ * @param locale
+ * the locale in which to display numbers
+ * @throws IllegalArgumentException
+ * if {@code locale} is {@code null}
+ */
+ public NumberRenderer(String formatString, Locale locale)
+ throws IllegalArgumentException {
+ this(formatString, locale, ""); // This will call #toString() during
+ // formatting
+ }
+
+ /**
+ * Creates a new number renderer.
+ * <p/>
+ * The renderer is configured to render with the given format string in the
+ * default locale.
+ *
+ * @param formatString
+ * the format string with which to format the number
+ * @throws IllegalArgumentException
+ * if {@code formatString} is {@code null}
+ * @see <a href=
+ * "http://docs.oracle.com/javase/7/docs/api/java/util/Formatter.html#syntax">Format
+ * String Syntax</a>
+ */
+ public NumberRenderer(String formatString) throws IllegalArgumentException {
+ this(formatString, Locale.getDefault(), "");
+ }
+
+ /**
+ * Creates a new number renderer.
+ * <p/>
+ * The renderer is configured to render with the given format string in the
+ * given locale.
+ *
+ * @param formatString
+ * the format string with which to format the number
+ * @param locale
+ * the locale in which to present numbers
+ * @throws IllegalArgumentException
+ * if either argument is {@code null}
+ * @see <a href=
+ * "http://docs.oracle.com/javase/7/docs/api/java/util/Formatter.html#syntax">Format
+ * String Syntax</a>
+ */
+ public NumberRenderer(String formatString, Locale locale,
+ String nullRepresentation) {
+ super(Number.class, nullRepresentation);
+
+ if (formatString == null) {
+ throw new IllegalArgumentException("Format string may not be null");
+ }
+
+ if (locale == null) {
+ throw new IllegalArgumentException("Locale may not be null");
+ }
+
+ this.locale = locale;
+ numberFormat = null;
+ this.formatString = formatString;
+ }
+
+ @Override
+ public JsonValue encode(Number value) {
+ String stringValue;
+ if (value == null) {
+ stringValue = getNullRepresentation();
+ } else if (formatString != null && locale != null) {
+ stringValue = String.format(locale, formatString, value);
+ } else if (numberFormat != null) {
+ stringValue = numberFormat.format(value);
+ } else {
+ throw new IllegalStateException(String.format(
+ "Internal bug: " + "%s is in an illegal state: "
+ + "[locale: %s, numberFormat: %s, formatString: %s]",
+ getClass().getSimpleName(), locale, numberFormat,
+ formatString));
+ }
+ return encode(stringValue, String.class);
+ }
+
+ @Override
+ public String toString() {
+ final String fieldInfo;
+ if (numberFormat != null) {
+ fieldInfo = "numberFormat: " + numberFormat.toString();
+ } else {
+ fieldInfo = "locale: " + locale + ", formatString: " + formatString;
+ }
+
+ return String.format("%s [%s]", getClass().getSimpleName(), fieldInfo);
+ }
+
+ @Override
+ public String getNullRepresentation() {
+ return super.getNullRepresentation();
+ }
+}
diff --git a/compatibility-server/src/main/java/com/vaadin/ui/renderers/ProgressBarRenderer.java b/compatibility-server/src/main/java/com/vaadin/ui/renderers/ProgressBarRenderer.java
new file mode 100644
index 0000000000..fe90dfdee0
--- /dev/null
+++ b/compatibility-server/src/main/java/com/vaadin/ui/renderers/ProgressBarRenderer.java
@@ -0,0 +1,46 @@
+/*
+ * Copyright 2000-2016 Vaadin Ltd.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not
+ * use this file except in compliance with the License. You may obtain a copy of
+ * the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+package com.vaadin.ui.renderers;
+
+import com.vaadin.ui.LegacyGrid.AbstractRenderer;
+
+import elemental.json.JsonValue;
+
+/**
+ * A renderer that represents a double values as a graphical progress bar.
+ *
+ * @author Vaadin Ltd
+ * @since 7.4
+ */
+public class ProgressBarRenderer extends AbstractRenderer<Double> {
+
+ /**
+ * Creates a new text renderer
+ */
+ public ProgressBarRenderer() {
+ super(Double.class, null);
+ }
+
+ @Override
+ public JsonValue encode(Double value) {
+ if (value != null) {
+ value = Math.max(Math.min(value, 1), 0);
+ } else {
+ value = 0d;
+ }
+ return super.encode(value);
+ }
+}
diff --git a/compatibility-server/src/main/java/com/vaadin/ui/renderers/Renderer.java b/compatibility-server/src/main/java/com/vaadin/ui/renderers/Renderer.java
new file mode 100644
index 0000000000..d42e299ea8
--- /dev/null
+++ b/compatibility-server/src/main/java/com/vaadin/ui/renderers/Renderer.java
@@ -0,0 +1,69 @@
+/*
+ * Copyright 2000-2016 Vaadin Ltd.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not
+ * use this file except in compliance with the License. You may obtain a copy of
+ * the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+package com.vaadin.ui.renderers;
+
+import com.vaadin.server.ClientConnector;
+import com.vaadin.server.Extension;
+
+import elemental.json.JsonValue;
+
+/**
+ * A ClientConnector for controlling client-side
+ * {@link com.vaadin.client.widget.grid.Renderer Grid renderers}. Renderers
+ * currently extend the Extension interface, but this fact should be regarded as
+ * an implementation detail and subject to change in a future major or minor
+ * Vaadin revision.
+ *
+ * @param <T>
+ * the type this renderer knows how to present
+ *
+ * @since 7.4
+ * @author Vaadin Ltd
+ */
+public interface Renderer<T> extends Extension {
+
+ /**
+ * Returns the class literal corresponding to the presentation type T.
+ *
+ * @return the class literal of T
+ */
+ Class<T> getPresentationType();
+
+ /**
+ * Encodes the given value into a {@link JsonValue}.
+ *
+ * @param value
+ * the value to encode
+ * @return a JSON representation of the given value
+ */
+ JsonValue encode(T value);
+
+ /**
+ * This method is inherited from Extension but should never be called
+ * directly with a Renderer.
+ */
+ @Override
+ @Deprecated
+ void remove();
+
+ /**
+ * This method is inherited from Extension but should never be called
+ * directly with a Renderer.
+ */
+ @Override
+ @Deprecated
+ void setParent(ClientConnector parent);
+}
diff --git a/compatibility-server/src/main/java/com/vaadin/ui/renderers/TextRenderer.java b/compatibility-server/src/main/java/com/vaadin/ui/renderers/TextRenderer.java
new file mode 100644
index 0000000000..ac73615272
--- /dev/null
+++ b/compatibility-server/src/main/java/com/vaadin/ui/renderers/TextRenderer.java
@@ -0,0 +1,50 @@
+/*
+ * Copyright 2000-2016 Vaadin Ltd.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not
+ * use this file except in compliance with the License. You may obtain a copy of
+ * the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+package com.vaadin.ui.renderers;
+
+import com.vaadin.ui.LegacyGrid.AbstractRenderer;
+import elemental.json.JsonValue;
+
+/**
+ * A renderer for presenting simple plain-text string values.
+ *
+ * @since 7.4
+ * @author Vaadin Ltd
+ */
+public class TextRenderer extends AbstractRenderer<String> {
+
+ /**
+ * Creates a new text renderer
+ */
+ public TextRenderer() {
+ this("");
+ }
+
+ /**
+ * Creates a new text renderer
+ *
+ * @param nullRepresentation
+ * the textual representation of {@code null} value
+ */
+ public TextRenderer(String nullRepresentation) {
+ super(String.class, nullRepresentation);
+ }
+
+ @Override
+ public String getNullRepresentation() {
+ return super.getNullRepresentation();
+ }
+}
diff --git a/compatibility-server/src/test/java/com/vaadin/data/fieldgroup/BeanFieldGroupTest.java b/compatibility-server/src/test/java/com/vaadin/data/fieldgroup/BeanFieldGroupTest.java
new file mode 100644
index 0000000000..3333cd7744
--- /dev/null
+++ b/compatibility-server/src/test/java/com/vaadin/data/fieldgroup/BeanFieldGroupTest.java
@@ -0,0 +1,70 @@
+/*
+ * Copyright 2012 Vaadin Ltd.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not
+ * use this file except in compliance with the License. You may obtain a copy of
+ * the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+package com.vaadin.data.fieldgroup;
+
+import org.junit.Assert;
+import org.junit.Test;
+
+public class BeanFieldGroupTest {
+
+ class Main {
+ private String mainField;
+
+ public String getMainField() {
+ return mainField;
+ }
+
+ public void setMainField(String mainField) {
+ this.mainField = mainField;
+ }
+
+ }
+
+ class Sub1 extends Main {
+ private Integer sub1Field;
+
+ public Integer getSub1Field() {
+ return sub1Field;
+ }
+
+ public void setSub1Field(Integer sub1Field) {
+ this.sub1Field = sub1Field;
+ }
+
+ }
+
+ class Sub2 extends Sub1 {
+ private boolean sub2field;
+
+ public boolean isSub2field() {
+ return sub2field;
+ }
+
+ public void setSub2field(boolean sub2field) {
+ this.sub2field = sub2field;
+ }
+
+ }
+
+ @Test
+ public void propertyTypeWithoutItem() {
+ BeanFieldGroup<Sub2> s = new BeanFieldGroup<BeanFieldGroupTest.Sub2>(
+ Sub2.class);
+ Assert.assertEquals(boolean.class, s.getPropertyType("sub2field"));
+ Assert.assertEquals(Integer.class, s.getPropertyType("sub1Field"));
+ Assert.assertEquals(String.class, s.getPropertyType("mainField"));
+ }
+}
diff --git a/compatibility-server/src/test/java/com/vaadin/data/fieldgroup/DefaultFieldGroupFieldFactoryTest.java b/compatibility-server/src/test/java/com/vaadin/data/fieldgroup/DefaultFieldGroupFieldFactoryTest.java
new file mode 100644
index 0000000000..93aa1350b0
--- /dev/null
+++ b/compatibility-server/src/test/java/com/vaadin/data/fieldgroup/DefaultFieldGroupFieldFactoryTest.java
@@ -0,0 +1,125 @@
+/*
+ * Copyright 2000-2016 Vaadin Ltd.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not
+ * use this file except in compliance with the License. You may obtain a copy of
+ * the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+package com.vaadin.data.fieldgroup;
+
+import java.lang.reflect.Constructor;
+import java.util.Date;
+
+import org.junit.Assert;
+import org.junit.Before;
+import org.junit.Test;
+
+import com.vaadin.data.fieldgroup.DefaultFieldGroupFieldFactory;
+import com.vaadin.ui.AbstractSelect;
+import com.vaadin.ui.ComboBox;
+import com.vaadin.ui.ListSelect;
+import com.vaadin.v7.ui.LegacyDateField;
+import com.vaadin.v7.ui.LegacyField;
+import com.vaadin.v7.ui.LegacyInlineDateField;
+import com.vaadin.v7.ui.LegacyPopupDateField;
+import com.vaadin.v7.ui.LegacyTextField;
+
+public class DefaultFieldGroupFieldFactoryTest {
+
+ private DefaultFieldGroupFieldFactory fieldFactory;
+
+ @Before
+ public void setupFieldFactory() {
+ fieldFactory = DefaultFieldGroupFieldFactory.get();
+ }
+
+ @Test
+ public void noPublicConstructor() {
+ Class<DefaultFieldGroupFieldFactory> clazz = DefaultFieldGroupFieldFactory.class;
+ Constructor<?>[] constructors = clazz.getConstructors();
+ Assert.assertEquals(
+ "DefaultFieldGroupFieldFactory contains public constructors", 0,
+ constructors.length);
+ }
+
+ @Test
+ public void testSameInstance() {
+ DefaultFieldGroupFieldFactory factory1 = DefaultFieldGroupFieldFactory
+ .get();
+ DefaultFieldGroupFieldFactory factory2 = DefaultFieldGroupFieldFactory
+ .get();
+ Assert.assertTrue(
+ "DefaultFieldGroupFieldFactory.get() method returns different instances",
+ factory1 == factory2);
+ Assert.assertNotNull(
+ "DefaultFieldGroupFieldFactory.get() method returns null",
+ factory1);
+ }
+
+ @Test
+ public void testDateGenerationForPopupDateField() {
+ LegacyField f = fieldFactory.createField(Date.class,
+ LegacyDateField.class);
+ Assert.assertNotNull(f);
+ Assert.assertEquals(LegacyPopupDateField.class, f.getClass());
+ }
+
+ @Test
+ public void testDateGenerationForInlineDateField() {
+ LegacyField f = fieldFactory.createField(Date.class,
+ LegacyInlineDateField.class);
+ Assert.assertNotNull(f);
+ Assert.assertEquals(LegacyInlineDateField.class, f.getClass());
+ }
+
+ @Test
+ public void testDateGenerationForTextField() {
+ LegacyField f = fieldFactory.createField(Date.class,
+ LegacyTextField.class);
+ Assert.assertNotNull(f);
+ Assert.assertEquals(LegacyTextField.class, f.getClass());
+ }
+
+ @Test
+ public void testDateGenerationForField() {
+ LegacyField f = fieldFactory.createField(Date.class, LegacyField.class);
+ Assert.assertNotNull(f);
+ Assert.assertEquals(LegacyPopupDateField.class, f.getClass());
+ }
+
+ public enum SomeEnum {
+ FOO, BAR;
+ }
+
+ @Test
+ public void testEnumComboBox() {
+ LegacyField f = fieldFactory.createField(SomeEnum.class,
+ ComboBox.class);
+ Assert.assertNotNull(f);
+ Assert.assertEquals(ComboBox.class, f.getClass());
+ }
+
+ @Test
+ public void testEnumAnySelect() {
+ LegacyField f = fieldFactory.createField(SomeEnum.class,
+ AbstractSelect.class);
+ Assert.assertNotNull(f);
+ Assert.assertEquals(ListSelect.class, f.getClass());
+ }
+
+ @Test
+ public void testEnumAnyField() {
+ LegacyField f = fieldFactory.createField(SomeEnum.class,
+ LegacyField.class);
+ Assert.assertNotNull(f);
+ Assert.assertEquals(ListSelect.class, f.getClass());
+ }
+}
diff --git a/compatibility-server/src/test/java/com/vaadin/data/fieldgroup/FieldGroupDateTest.java b/compatibility-server/src/test/java/com/vaadin/data/fieldgroup/FieldGroupDateTest.java
new file mode 100644
index 0000000000..e270211abf
--- /dev/null
+++ b/compatibility-server/src/test/java/com/vaadin/data/fieldgroup/FieldGroupDateTest.java
@@ -0,0 +1,97 @@
+/*
+ * Copyright 2000-2016 Vaadin Ltd.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not
+ * use this file except in compliance with the License. You may obtain a copy of
+ * the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+package com.vaadin.data.fieldgroup;
+
+import java.util.Date;
+
+import org.junit.Assert;
+import org.junit.Before;
+import org.junit.Test;
+
+import com.vaadin.data.util.BeanItem;
+import com.vaadin.v7.ui.LegacyField;
+import com.vaadin.v7.ui.LegacyPopupDateField;
+
+public class FieldGroupDateTest {
+
+ private FieldGroup fieldGroup;
+
+ public class TestBean {
+ private Date javaDate;
+ private java.sql.Date sqlDate;
+
+ public TestBean(Date javaDate, java.sql.Date sqlDate) {
+ super();
+ this.javaDate = javaDate;
+ this.sqlDate = sqlDate;
+ }
+
+ public java.sql.Date getSqlDate() {
+ return sqlDate;
+ }
+
+ public void setSqlDate(java.sql.Date sqlDate) {
+ this.sqlDate = sqlDate;
+ }
+
+ public Date getJavaDate() {
+ return javaDate;
+ }
+
+ public void setJavaDate(Date date) {
+ javaDate = date;
+ }
+ }
+
+ @SuppressWarnings("deprecation")
+ @Before
+ public void setup() {
+ fieldGroup = new FieldGroup();
+ fieldGroup.setItemDataSource(new BeanItem<TestBean>(new TestBean(
+ new Date(2010, 5, 7), new java.sql.Date(2011, 6, 8))));
+ }
+
+ @Test
+ public void testBuildAndBindDate() {
+ LegacyField f = fieldGroup.buildAndBind("javaDate");
+ Assert.assertNotNull(f);
+ Assert.assertEquals(LegacyPopupDateField.class, f.getClass());
+ }
+
+ @Test
+ public void testBuildAndBindSqlDate() {
+ LegacyField f = fieldGroup.buildAndBind("sqlDate");
+ Assert.assertNotNull(f);
+ Assert.assertEquals(LegacyPopupDateField.class, f.getClass());
+ }
+
+ @Test
+ public void clearFields() {
+ LegacyPopupDateField sqlDate = new LegacyPopupDateField();
+ LegacyPopupDateField javaDate = new LegacyPopupDateField();
+ fieldGroup.bind(sqlDate, "sqlDate");
+ fieldGroup.bind(javaDate, "javaDate");
+
+ Assert.assertEquals(new Date(2010, 5, 7), javaDate.getValue());
+ Assert.assertEquals(new Date(2011, 6, 8), sqlDate.getValue());
+
+ fieldGroup.clear();
+ Assert.assertEquals(null, javaDate.getValue());
+ Assert.assertEquals(null, sqlDate.getValue());
+
+ }
+
+}
diff --git a/compatibility-server/src/test/java/com/vaadin/data/fieldgroup/FieldGroupExceptionTest.java b/compatibility-server/src/test/java/com/vaadin/data/fieldgroup/FieldGroupExceptionTest.java
new file mode 100644
index 0000000000..4622f0960e
--- /dev/null
+++ b/compatibility-server/src/test/java/com/vaadin/data/fieldgroup/FieldGroupExceptionTest.java
@@ -0,0 +1,33 @@
+/*
+ * Copyright 2000-2016 Vaadin Ltd.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not
+ * use this file except in compliance with the License. You may obtain a copy of
+ * the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+package com.vaadin.data.fieldgroup;
+
+import org.junit.Test;
+
+import com.vaadin.data.fieldgroup.FieldGroup.CommitException;
+import com.vaadin.v7.ui.LegacyPopupDateField;
+
+public class FieldGroupExceptionTest {
+
+ @Test(expected = CommitException.class)
+ public void testUnboundCommitException() throws CommitException {
+ FieldGroup fieldGroup = new FieldGroup();
+ LegacyPopupDateField dateField = new LegacyPopupDateField();
+ fieldGroup.bind(dateField, "date");
+ fieldGroup.commit();
+ }
+
+}
diff --git a/compatibility-server/src/test/java/com/vaadin/data/fieldgroup/FieldGroupTest.java b/compatibility-server/src/test/java/com/vaadin/data/fieldgroup/FieldGroupTest.java
new file mode 100644
index 0000000000..c0039fc4fb
--- /dev/null
+++ b/compatibility-server/src/test/java/com/vaadin/data/fieldgroup/FieldGroupTest.java
@@ -0,0 +1,96 @@
+package com.vaadin.data.fieldgroup;
+
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.hamcrest.core.Is.is;
+import static org.hamcrest.core.IsNull.nullValue;
+import static org.mockito.Mockito.mock;
+
+import org.junit.Assert;
+import org.junit.Before;
+import org.junit.Test;
+
+import com.vaadin.data.Property;
+import com.vaadin.data.Property.Transactional;
+import com.vaadin.data.util.BeanItem;
+import com.vaadin.data.util.TransactionalPropertyWrapper;
+import com.vaadin.v7.ui.LegacyField;
+import com.vaadin.v7.ui.LegacyTextField;
+
+public class FieldGroupTest {
+
+ private FieldGroup sut;
+ private LegacyField field;
+
+ @Before
+ public void setup() {
+ sut = new FieldGroup();
+ field = mock(LegacyField.class);
+ }
+
+ @Test
+ public void fieldIsBound() {
+ sut.bind(field, "foobar");
+
+ assertThat(sut.getField("foobar"), is(field));
+ }
+
+ @Test(expected = FieldGroup.BindException.class)
+ public void cannotBindToAlreadyBoundProperty() {
+ sut.bind(field, "foobar");
+ sut.bind(mock(LegacyField.class), "foobar");
+ }
+
+ @Test(expected = FieldGroup.BindException.class)
+ public void cannotBindNullField() {
+ sut.bind(null, "foobar");
+ }
+
+ public void canUnbindWithoutItem() {
+ sut.bind(field, "foobar");
+
+ sut.unbind(field);
+ assertThat(sut.getField("foobar"), is(nullValue()));
+ }
+
+ @Test
+ public void wrapInTransactionalProperty_provideCustomImpl_customTransactionalWrapperIsUsed() {
+ Bean bean = new Bean();
+ FieldGroup group = new FieldGroup() {
+ @Override
+ protected <T> Transactional<T> wrapInTransactionalProperty(
+ Property<T> itemProperty) {
+ return new TransactionalPropertyImpl(itemProperty);
+ }
+ };
+ group.setItemDataSource(new BeanItem<Bean>(bean));
+ LegacyTextField field = new LegacyTextField();
+ group.bind(field, "name");
+
+ Property propertyDataSource = field.getPropertyDataSource();
+ Assert.assertTrue(
+ "Custom implementation of transactional property "
+ + "has not been used",
+ propertyDataSource instanceof TransactionalPropertyImpl);
+ }
+
+ public static class TransactionalPropertyImpl<T>
+ extends TransactionalPropertyWrapper<T> {
+
+ public TransactionalPropertyImpl(Property<T> wrappedProperty) {
+ super(wrappedProperty);
+ }
+
+ }
+
+ public static class Bean {
+ private String name;
+
+ public String getName() {
+ return name;
+ }
+
+ public void setName(String name) {
+ this.name = name;
+ }
+ }
+}
diff --git a/compatibility-server/src/test/java/com/vaadin/data/util/AbstractBeanContainerTestBase.java b/compatibility-server/src/test/java/com/vaadin/data/util/AbstractBeanContainerTestBase.java
new file mode 100644
index 0000000000..4a9e4f9891
--- /dev/null
+++ b/compatibility-server/src/test/java/com/vaadin/data/util/AbstractBeanContainerTestBase.java
@@ -0,0 +1,77 @@
+package com.vaadin.data.util;
+
+/**
+ * Automated test for {@link AbstractBeanContainer}.
+ *
+ * Only a limited subset of the functionality is tested here, the rest in tests
+ * of subclasses including {@link BeanItemContainer} and {@link BeanContainer}.
+ */
+public abstract class AbstractBeanContainerTestBase
+ extends AbstractInMemoryContainerTestBase {
+
+ public static class Person {
+ private String name;
+
+ public Person(String name) {
+ setName(name);
+ }
+
+ public void setName(String name) {
+ this.name = name;
+ }
+
+ public String getName() {
+ return name;
+ }
+ }
+
+ public static class ClassName {
+ // field names match constants in parent test class
+ private String fullyQualifiedName;
+ private String simpleName;
+ private String reverseFullyQualifiedName;
+ private Integer idNumber;
+
+ public ClassName(String fullyQualifiedName, Integer idNumber) {
+ this.fullyQualifiedName = fullyQualifiedName;
+ simpleName = AbstractContainerTestBase
+ .getSimpleName(fullyQualifiedName);
+ reverseFullyQualifiedName = reverse(fullyQualifiedName);
+ this.idNumber = idNumber;
+ }
+
+ public String getFullyQualifiedName() {
+ return fullyQualifiedName;
+ }
+
+ public void setFullyQualifiedName(String fullyQualifiedName) {
+ this.fullyQualifiedName = fullyQualifiedName;
+ }
+
+ public String getSimpleName() {
+ return simpleName;
+ }
+
+ public void setSimpleName(String simpleName) {
+ this.simpleName = simpleName;
+ }
+
+ public String getReverseFullyQualifiedName() {
+ return reverseFullyQualifiedName;
+ }
+
+ public void setReverseFullyQualifiedName(
+ String reverseFullyQualifiedName) {
+ this.reverseFullyQualifiedName = reverseFullyQualifiedName;
+ }
+
+ public Integer getIdNumber() {
+ return idNumber;
+ }
+
+ public void setIdNumber(Integer idNumber) {
+ this.idNumber = idNumber;
+ }
+ }
+
+}
diff --git a/compatibility-server/src/test/java/com/vaadin/data/util/AbstractContainerTestBase.java b/compatibility-server/src/test/java/com/vaadin/data/util/AbstractContainerTestBase.java
new file mode 100644
index 0000000000..955b609735
--- /dev/null
+++ b/compatibility-server/src/test/java/com/vaadin/data/util/AbstractContainerTestBase.java
@@ -0,0 +1,866 @@
+package com.vaadin.data.util;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertTrue;
+
+import java.util.ArrayList;
+import java.util.Iterator;
+import java.util.List;
+
+import org.junit.Assert;
+
+import com.vaadin.data.Container;
+import com.vaadin.data.Container.Filterable;
+import com.vaadin.data.Container.ItemSetChangeEvent;
+import com.vaadin.data.Container.ItemSetChangeListener;
+import com.vaadin.data.Container.Ordered;
+import com.vaadin.data.Container.Sortable;
+import com.vaadin.data.Item;
+import com.vaadin.data.util.filter.SimpleStringFilter;
+
+public abstract class AbstractContainerTestBase {
+
+ /**
+ * Helper class for testing e.g. listeners expecting events to be fired.
+ */
+ protected abstract static class AbstractEventCounter {
+ private int eventCount = 0;
+ private int lastAssertedEventCount = 0;
+
+ /**
+ * Increment the event count. To be called by subclasses e.g. from a
+ * listener method.
+ */
+ protected void increment() {
+ ++eventCount;
+ }
+
+ /**
+ * Check that no one event has occurred since the previous assert call.
+ */
+ public void assertNone() {
+ Assert.assertEquals(lastAssertedEventCount, eventCount);
+ }
+
+ /**
+ * Check that exactly one event has occurred since the previous assert
+ * call.
+ */
+ public void assertOnce() {
+ Assert.assertEquals(++lastAssertedEventCount, eventCount);
+ }
+
+ /**
+ * Reset the counter and the expected count.
+ */
+ public void reset() {
+ eventCount = 0;
+ lastAssertedEventCount = 0;
+ }
+ }
+
+ /**
+ * Test class for counting item set change events and verifying they have
+ * been received.
+ */
+ protected static class ItemSetChangeCounter extends AbstractEventCounter
+ implements ItemSetChangeListener {
+
+ @Override
+ public void containerItemSetChange(ItemSetChangeEvent event) {
+ increment();
+ }
+
+ }
+
+ // #6043: for items that have been filtered out, Container interface does
+ // not specify what to return from getItem() and getContainerProperty(), so
+ // need checkGetItemNull parameter for the test to be usable for most
+ // current containers
+ protected void validateContainer(Container container,
+ Object expectedFirstItemId, Object expectedLastItemId,
+ Object itemIdInSet, Object itemIdNotInSet, boolean checkGetItemNull,
+ int expectedSize) {
+ Container.Indexed indexed = null;
+ if (container instanceof Container.Indexed) {
+ indexed = (Container.Indexed) container;
+ }
+
+ List<Object> itemIdList = new ArrayList<Object>(container.getItemIds());
+
+ // size()
+ assertEquals(expectedSize, container.size());
+ assertEquals(expectedSize, itemIdList.size());
+
+ // first item, last item
+ Object first = itemIdList.get(0);
+ Object last = itemIdList.get(itemIdList.size() - 1);
+
+ assertEquals(expectedFirstItemId, first);
+ assertEquals(expectedLastItemId, last);
+
+ // containsId
+ assertFalse(container.containsId(itemIdNotInSet));
+ assertTrue(container.containsId(itemIdInSet));
+
+ // getItem
+ if (checkGetItemNull) {
+ assertNull(container.getItem(itemIdNotInSet));
+ }
+ assertNotNull(container.getItem(itemIdInSet));
+
+ // getContainerProperty
+ for (Object propId : container.getContainerPropertyIds()) {
+ if (checkGetItemNull) {
+ assertNull(
+ container.getContainerProperty(itemIdNotInSet, propId));
+ }
+ assertNotNull(container.getContainerProperty(itemIdInSet, propId));
+ }
+
+ if (indexed != null) {
+ // firstItemId
+ assertEquals(first, indexed.firstItemId());
+
+ // lastItemId
+ assertEquals(last, indexed.lastItemId());
+
+ // nextItemId
+ assertEquals(itemIdList.get(1), indexed.nextItemId(first));
+
+ // prevItemId
+ assertEquals(itemIdList.get(itemIdList.size() - 2),
+ indexed.prevItemId(last));
+
+ // isFirstId
+ assertTrue(indexed.isFirstId(first));
+ assertFalse(indexed.isFirstId(last));
+
+ // isLastId
+ assertTrue(indexed.isLastId(last));
+ assertFalse(indexed.isLastId(first));
+
+ // indexOfId
+ assertEquals(0, indexed.indexOfId(first));
+ assertEquals(expectedSize - 1, indexed.indexOfId(last));
+
+ // getIdByIndex
+ assertEquals(indexed.getIdByIndex(0), first);
+ assertEquals(indexed.getIdByIndex(expectedSize - 1), last);
+
+ }
+
+ // getItemProperty
+ Assert.assertNull(
+ container.getItem(itemIdInSet).getItemProperty("notinset"));
+
+ }
+
+ protected static final Object FULLY_QUALIFIED_NAME = "fullyQualifiedName";
+ protected static final Object SIMPLE_NAME = "simpleName";
+ protected static final Object REVERSE_FULLY_QUALIFIED_NAME = "reverseFullyQualifiedName";
+ protected static final Object ID_NUMBER = "idNumber";
+
+ protected void testBasicContainerOperations(Container container) {
+ initializeContainer(container);
+
+ // Basic container
+ validateContainer(container, sampleData[0],
+ sampleData[sampleData.length - 1], sampleData[10], "abc", true,
+ sampleData.length);
+
+ validateRemovingItems(container);
+ validateAddItem(container);
+ if (container instanceof Container.Indexed) {
+ validateAddItemAt((Container.Indexed) container);
+ }
+ if (container instanceof Container.Ordered) {
+ validateAddItemAfter((Container.Ordered) container);
+ }
+
+ }
+
+ protected void validateRemovingItems(Container container) {
+ int sizeBeforeRemoving = container.size();
+
+ List<Object> itemIdList = new ArrayList<Object>(container.getItemIds());
+ // There should be at least four items in the list
+ Object first = itemIdList.get(0);
+ Object middle = itemIdList.get(2);
+ Object last = itemIdList.get(itemIdList.size() - 1);
+
+ container.removeItem(first);
+ container.removeItem(middle); // Middle now that first has been removed
+ container.removeItem(last);
+
+ assertEquals(sizeBeforeRemoving - 3, container.size());
+
+ container.removeAllItems();
+
+ assertEquals(0, container.size());
+ }
+
+ protected void validateAddItem(Container container) {
+ try {
+ container.removeAllItems();
+
+ Object id = container.addItem();
+ Assert.assertTrue(container.containsId(id));
+ Assert.assertNotNull(container.getItem(id));
+
+ Item item = container.addItem("foo");
+ Assert.assertNotNull(item);
+ Assert.assertTrue(container.containsId("foo"));
+ Assert.assertEquals(item, container.getItem("foo"));
+
+ // Add again
+ Item item2 = container.addItem("foo");
+ Assert.assertNull(item2);
+
+ // Null is not a valid itemId
+ Assert.assertNull(container.addItem(null));
+ } catch (UnsupportedOperationException e) {
+ // Ignore contains which do not support addItem*
+ }
+ }
+
+ protected void validateAddItemAt(Container.Indexed container) {
+ try {
+ container.removeAllItems();
+
+ Object id = container.addItemAt(0);
+ Assert.assertTrue(container.containsId(id));
+ Assert.assertEquals(id, container.getIdByIndex(0));
+ Assert.assertNotNull(container.getItem(id));
+
+ Item item = container.addItemAt(0, "foo");
+ Assert.assertNotNull(item);
+ Assert.assertTrue(container.containsId("foo"));
+ Assert.assertEquals(item, container.getItem("foo"));
+ Assert.assertEquals("foo", container.getIdByIndex(0));
+
+ Item itemAtEnd = container.addItemAt(2, "atend");
+ Assert.assertNotNull(itemAtEnd);
+ Assert.assertTrue(container.containsId("atend"));
+ Assert.assertEquals(itemAtEnd, container.getItem("atend"));
+ Assert.assertEquals("atend", container.getIdByIndex(2));
+
+ // Add again
+ Item item2 = container.addItemAt(0, "foo");
+ Assert.assertNull(item2);
+ } catch (UnsupportedOperationException e) {
+ // Ignore contains which do not support addItem*
+ }
+ }
+
+ protected void validateAddItemAfter(Container.Ordered container) {
+ if (container instanceof AbstractBeanContainer) {
+ // Doesn't work as bean container requires beans
+ return;
+ }
+
+ try {
+ container.removeAllItems();
+
+ Assert.assertNotNull(container.addItem(0));
+
+ Item item = container.addItemAfter(null, "foo");
+ Assert.assertNotNull(item);
+ Assert.assertTrue(container.containsId("foo"));
+ Assert.assertEquals(item, container.getItem("foo"));
+ Assert.assertEquals("foo",
+ container.getItemIds().iterator().next());
+
+ Item itemAtEnd = container.addItemAfter(0, "atend");
+ Assert.assertNotNull(itemAtEnd);
+ Assert.assertTrue(container.containsId("atend"));
+ Assert.assertEquals(itemAtEnd, container.getItem("atend"));
+ Iterator<?> i = container.getItemIds().iterator();
+ i.next();
+ i.next();
+ Assert.assertEquals("atend", i.next());
+
+ // Add again
+ Assert.assertNull(container.addItemAfter(null, "foo"));
+ Assert.assertNull(container.addItemAfter("atend", "foo"));
+ Assert.assertNull(container.addItemAfter("nonexistant", "123123"));
+ } catch (UnsupportedOperationException e) {
+ // Ignore contains which do not support addItem*
+ }
+ }
+
+ protected void testContainerOrdered(Container.Ordered container) {
+ // addItem with empty container
+ Object id = container.addItem();
+ assertOrderedContents(container, id);
+ Item item = container.getItem(id);
+ assertNotNull(item);
+
+ // addItemAfter with empty container
+ container.removeAllItems();
+ assertOrderedContents(container);
+ id = container.addItemAfter(null);
+ assertOrderedContents(container, id);
+ item = container.getItem(id);
+ assertNotNull(item);
+
+ // Add a new item before the first
+ // addItemAfter
+ Object newFirstId = container.addItemAfter(null);
+ assertOrderedContents(container, newFirstId, id);
+
+ // addItemAfter(Object)
+ Object newSecondItemId = container.addItemAfter(newFirstId);
+ // order is now: newFirstId, newSecondItemId, id
+ assertOrderedContents(container, newFirstId, newSecondItemId, id);
+
+ // addItemAfter(Object,Object)
+ String fourthId = "id of the fourth item";
+ Item fourth = container.addItemAfter(newFirstId, fourthId);
+ // order is now: newFirstId, fourthId, newSecondItemId, id
+ assertNotNull(fourth);
+ assertEquals(fourth, container.getItem(fourthId));
+ assertOrderedContents(container, newFirstId, fourthId, newSecondItemId,
+ id);
+
+ // addItemAfter(Object,Object)
+ Object fifthId = new Object();
+ Item fifth = container.addItemAfter(null, fifthId);
+ // order is now: fifthId, newFirstId, fourthId, newSecondItemId, id
+ assertNotNull(fifth);
+ assertEquals(fifth, container.getItem(fifthId));
+ assertOrderedContents(container, fifthId, newFirstId, fourthId,
+ newSecondItemId, id);
+
+ // addItemAfter(Object,Object)
+ Object sixthId = new Object();
+ Item sixth = container.addItemAfter(id, sixthId);
+ // order is now: fifthId, newFirstId, fourthId, newSecondItemId, id,
+ // sixthId
+ assertNotNull(sixth);
+ assertEquals(sixth, container.getItem(sixthId));
+ assertOrderedContents(container, fifthId, newFirstId, fourthId,
+ newSecondItemId, id, sixthId);
+
+ // Test order after removing first item 'fifthId'
+ container.removeItem(fifthId);
+ // order is now: newFirstId, fourthId, newSecondItemId, id, sixthId
+ assertOrderedContents(container, newFirstId, fourthId, newSecondItemId,
+ id, sixthId);
+
+ // Test order after removing last item 'sixthId'
+ container.removeItem(sixthId);
+ // order is now: newFirstId, fourthId, newSecondItemId, id
+ assertOrderedContents(container, newFirstId, fourthId, newSecondItemId,
+ id);
+
+ // Test order after removing item from the middle 'fourthId'
+ container.removeItem(fourthId);
+ // order is now: newFirstId, newSecondItemId, id
+ assertOrderedContents(container, newFirstId, newSecondItemId, id);
+
+ // Delete remaining items
+ container.removeItem(newFirstId);
+ container.removeItem(newSecondItemId);
+ container.removeItem(id);
+ assertOrderedContents(container);
+
+ Object finalItem = container.addItem();
+ assertOrderedContents(container, finalItem);
+ }
+
+ private void assertOrderedContents(Ordered container, Object... ids) {
+ assertEquals(ids.length, container.size());
+ for (int i = 0; i < ids.length - 1; i++) {
+ assertNotNull("The item id should not be null", ids[i]);
+ }
+ if (ids.length == 0) {
+ assertNull("The first id is wrong", container.firstItemId());
+ assertNull("The last id is wrong", container.lastItemId());
+ return;
+ }
+
+ assertEquals("The first id is wrong", ids[0], container.firstItemId());
+ assertEquals("The last id is wrong", ids[ids.length - 1],
+ container.lastItemId());
+
+ // isFirstId & isLastId
+ assertTrue(container.isFirstId(container.firstItemId()));
+ assertTrue(container.isLastId(container.lastItemId()));
+
+ // nextId
+ Object ref = container.firstItemId();
+ for (int i = 1; i < ids.length; i++) {
+ Object next = container.nextItemId(ref);
+ assertEquals("The id after " + ref + " is wrong", ids[i], next);
+ ref = next;
+ }
+ assertNull("The last id should not have a next id",
+ container.nextItemId(ids[ids.length - 1]));
+ assertNull(container.nextItemId("not-in-container"));
+
+ // prevId
+ ref = container.lastItemId();
+ for (int i = ids.length - 2; i >= 0; i--) {
+ Object prev = container.prevItemId(ref);
+ assertEquals("The id before " + ref + " is wrong", ids[i], prev);
+ ref = prev;
+ }
+ assertNull("The first id should not have a prev id",
+ container.prevItemId(ids[0]));
+ assertNull(container.prevItemId("not-in-container"));
+
+ }
+
+ protected void testContainerIndexed(Container.Indexed container,
+ Object itemId, int itemPosition, boolean testAddEmptyItemAt,
+ Object newItemId, boolean testAddItemAtWithId) {
+ initializeContainer(container);
+
+ // indexOfId
+ Assert.assertEquals(itemPosition, container.indexOfId(itemId));
+
+ // getIdByIndex
+ Assert.assertEquals(itemId, container.getIdByIndex(itemPosition));
+
+ // addItemAt
+ if (testAddEmptyItemAt) {
+ Object addedId = container.addItemAt(itemPosition);
+ Assert.assertEquals(itemPosition, container.indexOfId(addedId));
+ Assert.assertEquals(itemPosition + 1, container.indexOfId(itemId));
+ Assert.assertEquals(addedId, container.getIdByIndex(itemPosition));
+ Assert.assertEquals(itemId,
+ container.getIdByIndex(itemPosition + 1));
+
+ Object newFirstId = container.addItemAt(0);
+ Assert.assertEquals(0, container.indexOfId(newFirstId));
+ Assert.assertEquals(itemPosition + 2, container.indexOfId(itemId));
+ Assert.assertEquals(newFirstId, container.firstItemId());
+ Assert.assertEquals(newFirstId, container.getIdByIndex(0));
+ Assert.assertEquals(itemId,
+ container.getIdByIndex(itemPosition + 2));
+
+ Object newLastId = container.addItemAt(container.size());
+ Assert.assertEquals(container.size() - 1,
+ container.indexOfId(newLastId));
+ Assert.assertEquals(itemPosition + 2, container.indexOfId(itemId));
+ Assert.assertEquals(newLastId, container.lastItemId());
+ Assert.assertEquals(newLastId,
+ container.getIdByIndex(container.size() - 1));
+ Assert.assertEquals(itemId,
+ container.getIdByIndex(itemPosition + 2));
+
+ Assert.assertTrue(container.removeItem(addedId));
+ Assert.assertTrue(container.removeItem(newFirstId));
+ Assert.assertTrue(container.removeItem(newLastId));
+
+ Assert.assertFalse(
+ "Removing non-existing item should indicate failure",
+ container.removeItem(addedId));
+ }
+
+ // addItemAt
+ if (testAddItemAtWithId) {
+ container.addItemAt(itemPosition, newItemId);
+ Assert.assertEquals(itemPosition, container.indexOfId(newItemId));
+ Assert.assertEquals(itemPosition + 1, container.indexOfId(itemId));
+ Assert.assertEquals(newItemId,
+ container.getIdByIndex(itemPosition));
+ Assert.assertEquals(itemId,
+ container.getIdByIndex(itemPosition + 1));
+ Assert.assertTrue(container.removeItem(newItemId));
+ Assert.assertFalse(container.containsId(newItemId));
+
+ container.addItemAt(0, newItemId);
+ Assert.assertEquals(0, container.indexOfId(newItemId));
+ Assert.assertEquals(itemPosition + 1, container.indexOfId(itemId));
+ Assert.assertEquals(newItemId, container.firstItemId());
+ Assert.assertEquals(newItemId, container.getIdByIndex(0));
+ Assert.assertEquals(itemId,
+ container.getIdByIndex(itemPosition + 1));
+ Assert.assertTrue(container.removeItem(newItemId));
+ Assert.assertFalse(container.containsId(newItemId));
+
+ container.addItemAt(container.size(), newItemId);
+ Assert.assertEquals(container.size() - 1,
+ container.indexOfId(newItemId));
+ Assert.assertEquals(itemPosition, container.indexOfId(itemId));
+ Assert.assertEquals(newItemId, container.lastItemId());
+ Assert.assertEquals(newItemId,
+ container.getIdByIndex(container.size() - 1));
+ Assert.assertEquals(itemId, container.getIdByIndex(itemPosition));
+ Assert.assertTrue(container.removeItem(newItemId));
+ Assert.assertFalse(container.containsId(newItemId));
+ }
+ }
+
+ protected void testContainerFiltering(Container.Filterable container) {
+ initializeContainer(container);
+
+ // Filter by "contains ab"
+ SimpleStringFilter filter1 = new SimpleStringFilter(
+ FULLY_QUALIFIED_NAME, "ab", false, false);
+ container.addContainerFilter(filter1);
+
+ assertTrue(container.getContainerFilters().size() == 1);
+ assertEquals(filter1,
+ container.getContainerFilters().iterator().next());
+
+ validateContainer(container, "com.vaadin.data.BufferedValidatable",
+ "com.vaadin.ui.TabSheet",
+ "com.vaadin.terminal.gwt.client.Focusable",
+ "com.vaadin.data.Buffered", isFilteredOutItemNull(), 20);
+
+ // Filter by "contains da" (reversed as ad here)
+ container.removeAllContainerFilters();
+
+ assertTrue(container.getContainerFilters().isEmpty());
+
+ SimpleStringFilter filter2 = new SimpleStringFilter(
+ REVERSE_FULLY_QUALIFIED_NAME, "ad", false, false);
+ container.addContainerFilter(filter2);
+
+ assertTrue(container.getContainerFilters().size() == 1);
+ assertEquals(filter2,
+ container.getContainerFilters().iterator().next());
+
+ validateContainer(container, "com.vaadin.data.Buffered",
+ "com.vaadin.server.ComponentSizeValidator",
+ "com.vaadin.data.util.IndexedContainer",
+ "com.vaadin.terminal.gwt.client.ui.VUriFragmentUtility",
+ isFilteredOutItemNull(), 37);
+ }
+
+ /**
+ * Override in subclasses to return false if the container getItem() method
+ * returns a non-null value for an item that has been filtered out.
+ *
+ * @return
+ */
+ protected boolean isFilteredOutItemNull() {
+ return true;
+ }
+
+ protected void testContainerSortingAndFiltering(
+ Container.Sortable sortable) {
+ Filterable filterable = (Filterable) sortable;
+
+ initializeContainer(sortable);
+
+ // Filter by "contains ab"
+ filterable.addContainerFilter(new SimpleStringFilter(
+ FULLY_QUALIFIED_NAME, "ab", false, false));
+
+ // Must be able to sort based on PROP1 for this test
+ assertTrue(sortable.getSortableContainerPropertyIds()
+ .contains(FULLY_QUALIFIED_NAME));
+
+ sortable.sort(new Object[] { FULLY_QUALIFIED_NAME },
+ new boolean[] { true });
+
+ validateContainer(sortable, "com.vaadin.data.BufferedValidatable",
+ "com.vaadin.ui.TableFieldFactory",
+ "com.vaadin.ui.TableFieldFactory",
+ "com.vaadin.data.util.BeanItem", isFilteredOutItemNull(), 20);
+ }
+
+ protected void testContainerSorting(Container.Filterable container) {
+ Container.Sortable sortable = (Sortable) container;
+
+ initializeContainer(container);
+
+ // Must be able to sort based on PROP1 for this test
+ assertTrue(sortable.getSortableContainerPropertyIds()
+ .contains(FULLY_QUALIFIED_NAME));
+ assertTrue(sortable.getSortableContainerPropertyIds()
+ .contains(REVERSE_FULLY_QUALIFIED_NAME));
+
+ sortable.sort(new Object[] { FULLY_QUALIFIED_NAME },
+ new boolean[] { true });
+
+ validateContainer(container, "com.vaadin.Application",
+ "org.vaadin.test.LastClass",
+ "com.vaadin.server.ApplicationResource", "blah", true,
+ sampleData.length);
+
+ sortable.sort(new Object[] { REVERSE_FULLY_QUALIFIED_NAME },
+ new boolean[] { true });
+
+ validateContainer(container, "com.vaadin.server.ApplicationPortlet2",
+ "com.vaadin.data.util.ObjectProperty",
+ "com.vaadin.ui.BaseFieldFactory", "blah", true,
+ sampleData.length);
+
+ }
+
+ protected void initializeContainer(Container container) {
+ Assert.assertTrue(container.removeAllItems());
+ Object[] propertyIds = container.getContainerPropertyIds().toArray();
+ for (Object propertyId : propertyIds) {
+ container.removeContainerProperty(propertyId);
+ }
+
+ container.addContainerProperty(FULLY_QUALIFIED_NAME, String.class, "");
+ container.addContainerProperty(SIMPLE_NAME, String.class, "");
+ container.addContainerProperty(REVERSE_FULLY_QUALIFIED_NAME,
+ String.class, null);
+ container.addContainerProperty(ID_NUMBER, Integer.class, null);
+
+ for (int i = 0; i < sampleData.length; i++) {
+ String id = sampleData[i];
+ Item item = container.addItem(id);
+
+ item.getItemProperty(FULLY_QUALIFIED_NAME).setValue(sampleData[i]);
+ item.getItemProperty(SIMPLE_NAME)
+ .setValue(getSimpleName(sampleData[i]));
+ item.getItemProperty(REVERSE_FULLY_QUALIFIED_NAME)
+ .setValue(reverse(sampleData[i]));
+ item.getItemProperty(ID_NUMBER).setValue(i);
+ }
+ }
+
+ protected static String getSimpleName(String name) {
+ if (name.contains(".")) {
+ return name.substring(name.lastIndexOf('.') + 1);
+ } else {
+ return name;
+ }
+ }
+
+ protected static String reverse(String string) {
+ return new StringBuilder(string).reverse().toString();
+ }
+
+ protected final String[] sampleData = {
+ "com.vaadin.annotations.AutoGenerated", "com.vaadin.Application",
+ "com.vaadin.data.Buffered", "com.vaadin.data.BufferedValidatable",
+ "com.vaadin.data.Container", "com.vaadin.data.Item",
+ "com.vaadin.data.Property", "com.vaadin.data.util.BeanItem",
+ "com.vaadin.data.util.BeanItemContainer",
+ "com.vaadin.data.util.ContainerHierarchicalWrapper",
+ "com.vaadin.data.util.ContainerOrderedWrapper",
+ "com.vaadin.data.util.DefaultItemSorter",
+ "com.vaadin.data.util.FilesystemContainer",
+ "com.vaadin.data.util.Filter",
+ "com.vaadin.data.util.HierarchicalContainer",
+ "com.vaadin.data.util.IndexedContainer",
+ "com.vaadin.data.util.ItemSorter",
+ "com.vaadin.data.util.MethodProperty",
+ "com.vaadin.data.util.ObjectProperty",
+ "com.vaadin.data.util.PropertyFormatter",
+ "com.vaadin.data.util.PropertysetItem",
+ "com.vaadin.data.util.QueryContainer",
+ "com.vaadin.data.util.TextFileProperty",
+ "com.vaadin.data.Validatable",
+ "com.vaadin.data.validator.AbstractStringValidator",
+ "com.vaadin.data.validator.AbstractValidator",
+ "com.vaadin.data.validator.CompositeValidator",
+ "com.vaadin.data.validator.DoubleValidator",
+ "com.vaadin.data.validator.EmailValidator",
+ "com.vaadin.data.validator.IntegerValidator",
+ "com.vaadin.data.validator.NullValidator",
+ "com.vaadin.data.validator.RegexpValidator",
+ "com.vaadin.data.validator.StringLengthValidator",
+ "com.vaadin.data.Validator", "com.vaadin.event.Action",
+ "com.vaadin.event.ComponentEventListener",
+ "com.vaadin.event.EventRouter", "com.vaadin.event.FieldEvents",
+ "com.vaadin.event.ItemClickEvent", "com.vaadin.event.LayoutEvents",
+ "com.vaadin.event.ListenerMethod",
+ "com.vaadin.event.MethodEventSource",
+ "com.vaadin.event.MouseEvents", "com.vaadin.event.ShortcutAction",
+ "com.vaadin.launcher.DemoLauncher",
+ "com.vaadin.launcher.DevelopmentServerLauncher",
+ "com.vaadin.launcher.util.BrowserLauncher",
+ "com.vaadin.service.ApplicationContext",
+ "com.vaadin.service.FileTypeResolver",
+ "com.vaadin.server.ApplicationResource",
+ "com.vaadin.server.ClassResource",
+ "com.vaadin.server.CompositeErrorMessage",
+ "com.vaadin.server.DownloadStream",
+ "com.vaadin.server.ErrorMessage",
+ "com.vaadin.server.ExternalResource",
+ "com.vaadin.server.FileResource",
+ "com.vaadin.terminal.gwt.client.ApplicationConfiguration",
+ "com.vaadin.terminal.gwt.client.ApplicationConnection",
+ "com.vaadin.terminal.gwt.client.BrowserInfo",
+ "com.vaadin.terminal.gwt.client.ClientExceptionHandler",
+ "com.vaadin.terminal.gwt.client.ComponentDetail",
+ "com.vaadin.terminal.gwt.client.ComponentDetailMap",
+ "com.vaadin.terminal.gwt.client.ComponentLocator",
+ "com.vaadin.terminal.gwt.client.Console",
+ "com.vaadin.terminal.gwt.client.Container",
+ "com.vaadin.terminal.gwt.client.ContainerResizedListener",
+ "com.vaadin.terminal.gwt.client.CSSRule",
+ "com.vaadin.terminal.gwt.client.DateTimeService",
+ "com.vaadin.terminal.gwt.client.DefaultWidgetSet",
+ "com.vaadin.terminal.gwt.client.Focusable",
+ "com.vaadin.terminal.gwt.client.HistoryImplIEVaadin",
+ "com.vaadin.terminal.gwt.client.LocaleNotLoadedException",
+ "com.vaadin.terminal.gwt.client.LocaleService",
+ "com.vaadin.terminal.gwt.client.MouseEventDetails",
+ "com.vaadin.terminal.gwt.client.NullConsole",
+ "com.vaadin.terminal.gwt.client.Paintable",
+ "com.vaadin.terminal.gwt.client.RenderInformation",
+ "com.vaadin.terminal.gwt.client.RenderSpace",
+ "com.vaadin.terminal.gwt.client.StyleConstants",
+ "com.vaadin.terminal.gwt.client.TooltipInfo",
+ "com.vaadin.terminal.gwt.client.ui.Action",
+ "com.vaadin.terminal.gwt.client.ui.ActionOwner",
+ "com.vaadin.terminal.gwt.client.ui.AlignmentInfo",
+ "com.vaadin.terminal.gwt.client.ui.CalendarEntry",
+ "com.vaadin.terminal.gwt.client.ui.ClickEventHandler",
+ "com.vaadin.terminal.gwt.client.ui.Field",
+ "com.vaadin.terminal.gwt.client.ui.Icon",
+ "com.vaadin.terminal.gwt.client.ui.layout.CellBasedLayout",
+ "com.vaadin.terminal.gwt.client.ui.layout.ChildComponentContainer",
+ "com.vaadin.terminal.gwt.client.ui.layout.Margins",
+ "com.vaadin.terminal.gwt.client.ui.LayoutClickEventHandler",
+ "com.vaadin.terminal.gwt.client.ui.MenuBar",
+ "com.vaadin.terminal.gwt.client.ui.MenuItem",
+ "com.vaadin.terminal.gwt.client.ui.richtextarea.VRichTextToolbar",
+ "com.vaadin.terminal.gwt.client.ui.ShortcutActionHandler",
+ "com.vaadin.terminal.gwt.client.ui.SubPartAware",
+ "com.vaadin.terminal.gwt.client.ui.Table",
+ "com.vaadin.terminal.gwt.client.ui.TreeAction",
+ "com.vaadin.terminal.gwt.client.ui.TreeImages",
+ "com.vaadin.terminal.gwt.client.ui.VAbsoluteLayout",
+ "com.vaadin.terminal.gwt.client.ui.VAccordion",
+ "com.vaadin.terminal.gwt.client.ui.VButton",
+ "com.vaadin.terminal.gwt.client.ui.VCalendarPanel",
+ "com.vaadin.terminal.gwt.client.ui.VCheckBox",
+ "com.vaadin.terminal.gwt.client.ui.VContextMenu",
+ "com.vaadin.terminal.gwt.client.ui.VCssLayout",
+ "com.vaadin.terminal.gwt.client.ui.VCustomComponent",
+ "com.vaadin.terminal.gwt.client.ui.VCustomLayout",
+ "com.vaadin.terminal.gwt.client.ui.VDateField",
+ "com.vaadin.terminal.gwt.client.ui.VDateFieldCalendar",
+ "com.vaadin.terminal.gwt.client.ui.VEmbedded",
+ "com.vaadin.terminal.gwt.client.ui.VFilterSelect",
+ "com.vaadin.terminal.gwt.client.ui.VForm",
+ "com.vaadin.terminal.gwt.client.ui.VFormLayout",
+ "com.vaadin.terminal.gwt.client.ui.VGridLayout",
+ "com.vaadin.terminal.gwt.client.ui.VHorizontalLayout",
+ "com.vaadin.terminal.gwt.client.ui.VLabel",
+ "com.vaadin.terminal.gwt.client.ui.VLink",
+ "com.vaadin.terminal.gwt.client.ui.VListSelect",
+ "com.vaadin.terminal.gwt.client.ui.VMarginInfo",
+ "com.vaadin.terminal.gwt.client.ui.VMenuBar",
+ "com.vaadin.terminal.gwt.client.ui.VNativeButton",
+ "com.vaadin.terminal.gwt.client.ui.VNativeSelect",
+ "com.vaadin.terminal.gwt.client.ui.VNotification",
+ "com.vaadin.terminal.gwt.client.ui.VOptionGroup",
+ "com.vaadin.terminal.gwt.client.ui.VOptionGroupBase",
+ "com.vaadin.terminal.gwt.client.ui.VOrderedLayout",
+ "com.vaadin.terminal.gwt.client.ui.VOverlay",
+ "com.vaadin.terminal.gwt.client.ui.VPanel",
+ "com.vaadin.terminal.gwt.client.ui.VPasswordField",
+ "com.vaadin.terminal.gwt.client.ui.VPopupCalendar",
+ "com.vaadin.terminal.gwt.client.ui.VPopupView",
+ "com.vaadin.terminal.gwt.client.ui.VProgressIndicator",
+ "com.vaadin.terminal.gwt.client.ui.VRichTextArea",
+ "com.vaadin.terminal.gwt.client.ui.VScrollTable",
+ "com.vaadin.terminal.gwt.client.ui.VSlider",
+ "com.vaadin.terminal.gwt.client.ui.VSplitPanel",
+ "com.vaadin.terminal.gwt.client.ui.VSplitPanelHorizontal",
+ "com.vaadin.terminal.gwt.client.ui.VSplitPanelVertical",
+ "com.vaadin.terminal.gwt.client.ui.VTablePaging",
+ "com.vaadin.terminal.gwt.client.ui.VTabsheet",
+ "com.vaadin.terminal.gwt.client.ui.VTabsheetBase",
+ "com.vaadin.terminal.gwt.client.ui.VTabsheetPanel",
+ "com.vaadin.terminal.gwt.client.ui.VTextArea",
+ "com.vaadin.terminal.gwt.client.ui.VTextField",
+ "com.vaadin.terminal.gwt.client.ui.VTextualDate",
+ "com.vaadin.terminal.gwt.client.ui.VTime",
+ "com.vaadin.terminal.gwt.client.ui.VTree",
+ "com.vaadin.terminal.gwt.client.ui.VTwinColSelect",
+ "com.vaadin.terminal.gwt.client.ui.VUnknownComponent",
+ "com.vaadin.terminal.gwt.client.ui.VUpload",
+ "com.vaadin.terminal.gwt.client.ui.VUriFragmentUtility",
+ "com.vaadin.terminal.gwt.client.ui.VVerticalLayout",
+ "com.vaadin.terminal.gwt.client.ui.VView",
+ "com.vaadin.terminal.gwt.client.ui.VWindow",
+ "com.vaadin.terminal.gwt.client.UIDL",
+ "com.vaadin.terminal.gwt.client.Util",
+ "com.vaadin.terminal.gwt.client.ValueMap",
+ "com.vaadin.terminal.gwt.client.VCaption",
+ "com.vaadin.terminal.gwt.client.VCaptionWrapper",
+ "com.vaadin.terminal.gwt.client.VDebugConsole",
+ "com.vaadin.terminal.gwt.client.VErrorMessage",
+ "com.vaadin.terminal.gwt.client.VTooltip",
+ "com.vaadin.terminal.gwt.client.VUIDLBrowser",
+ "com.vaadin.terminal.gwt.client.WidgetMap",
+ "com.vaadin.terminal.gwt.client.WidgetSet",
+ "com.vaadin.server.AbstractApplicationPortlet",
+ "com.vaadin.server.AbstractApplicationServlet",
+ "com.vaadin.server.AbstractCommunicationManager",
+ "com.vaadin.server.AbstractWebApplicationContext",
+ "com.vaadin.server.ApplicationPortlet",
+ "com.vaadin.server.ApplicationPortlet2",
+ "com.vaadin.server.ApplicationRunnerServlet",
+ "com.vaadin.server.ApplicationServlet",
+ "com.vaadin.server.ChangeVariablesErrorEvent",
+ "com.vaadin.server.CommunicationManager",
+ "com.vaadin.server.ComponentSizeValidator",
+ "com.vaadin.server.Constants",
+ "com.vaadin.server.GAEApplicationServlet",
+ "com.vaadin.server.HttpServletRequestListener",
+ "com.vaadin.server.HttpUploadStream",
+ "com.vaadin.server.JsonPaintTarget",
+ "com.vaadin.server.PortletApplicationContext",
+ "com.vaadin.server.PortletApplicationContext2",
+ "com.vaadin.server.PortletCommunicationManager",
+ "com.vaadin.server.PortletRequestListener",
+ "com.vaadin.server.RestrictedRenderResponse",
+ "com.vaadin.server.SessionExpiredException",
+ "com.vaadin.server.SystemMessageException",
+ "com.vaadin.server.WebApplicationContext",
+ "com.vaadin.server.WebBrowser",
+ "com.vaadin.server.widgetsetutils.ClassPathExplorer",
+ "com.vaadin.server.widgetsetutils.WidgetMapGenerator",
+ "com.vaadin.server.widgetsetutils.WidgetSetBuilder",
+ "com.vaadin.server.KeyMapper", "com.vaadin.server.Paintable",
+ "com.vaadin.server.PaintException", "com.vaadin.server.PaintTarget",
+ "com.vaadin.server.ParameterHandler", "com.vaadin.server.Resource",
+ "com.vaadin.server.Scrollable", "com.vaadin.server.Sizeable",
+ "com.vaadin.server.StreamResource", "com.vaadin.server.SystemError",
+ "com.vaadin.server.Terminal", "com.vaadin.server.ThemeResource",
+ "com.vaadin.server.UploadStream", "com.vaadin.server.URIHandler",
+ "com.vaadin.server.UserError", "com.vaadin.server.VariableOwner",
+ "com.vaadin.tools.ReflectTools",
+ "com.vaadin.tools.WidgetsetCompiler",
+ "com.vaadin.ui.AbsoluteLayout", "com.vaadin.ui.AbstractComponent",
+ "com.vaadin.ui.AbstractComponentContainer",
+ "com.vaadin.ui.AbstractField", "com.vaadin.ui.AbstractLayout",
+ "com.vaadin.ui.AbstractOrderedLayout",
+ "com.vaadin.ui.AbstractSelect", "com.vaadin.ui.Accordion",
+ "com.vaadin.ui.Alignment", "com.vaadin.ui.AlignmentUtils",
+ "com.vaadin.ui.BaseFieldFactory", "com.vaadin.ui.Button",
+ "com.vaadin.ui.CheckBox", "com.vaadin.ui.ClientWidget",
+ "com.vaadin.ui.ComboBox", "com.vaadin.ui.Component",
+ "com.vaadin.ui.ComponentContainer", "com.vaadin.ui.CssLayout",
+ "com.vaadin.ui.CustomComponent", "com.vaadin.ui.CustomLayout",
+ "com.vaadin.ui.DateField", "com.vaadin.ui.DefaultFieldFactory",
+ "com.vaadin.ui.Embedded", "com.vaadin.ui.ExpandLayout",
+ "com.vaadin.ui.Field", "com.vaadin.ui.FieldFactory",
+ "com.vaadin.ui.Form", "com.vaadin.ui.FormFieldFactory",
+ "com.vaadin.ui.FormLayout", "com.vaadin.ui.GridLayout",
+ "com.vaadin.ui.HorizontalLayout", "com.vaadin.ui.InlineDateField",
+ "com.vaadin.ui.Label", "com.vaadin.ui.Layout", "com.vaadin.ui.Link",
+ "com.vaadin.ui.ListSelect", "com.vaadin.ui.LoginForm",
+ "com.vaadin.ui.MenuBar", "com.vaadin.ui.NativeButton",
+ "com.vaadin.ui.NativeSelect", "com.vaadin.ui.OptionGroup",
+ "com.vaadin.ui.OrderedLayout", "com.vaadin.ui.Panel",
+ "com.vaadin.ui.PopupDateField", "com.vaadin.ui.PopupView",
+ "com.vaadin.ui.ProgressIndicator", "com.vaadin.ui.RichTextArea",
+ "com.vaadin.ui.Select", "com.vaadin.ui.Slider",
+ "com.vaadin.ui.SplitPanel", "com.vaadin.ui.Table",
+ "com.vaadin.ui.TableFieldFactory", "com.vaadin.ui.TabSheet",
+ "com.vaadin.ui.TextField", "com.vaadin.ui.Tree",
+ "com.vaadin.ui.TwinColSelect", "com.vaadin.ui.Upload",
+ "com.vaadin.ui.UriFragmentUtility", "com.vaadin.ui.VerticalLayout",
+ "com.vaadin.ui.Window", "com.vaadin.util.SerializerHelper",
+ "org.vaadin.test.LastClass" };
+}
diff --git a/compatibility-server/src/test/java/com/vaadin/data/util/AbstractHierarchicalContainerTestBase.java b/compatibility-server/src/test/java/com/vaadin/data/util/AbstractHierarchicalContainerTestBase.java
new file mode 100644
index 0000000000..f3eda74100
--- /dev/null
+++ b/compatibility-server/src/test/java/com/vaadin/data/util/AbstractHierarchicalContainerTestBase.java
@@ -0,0 +1,289 @@
+package com.vaadin.data.util;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertTrue;
+
+import java.util.Collection;
+
+import com.vaadin.data.Container;
+import com.vaadin.data.Container.Hierarchical;
+import com.vaadin.data.Container.Sortable;
+import com.vaadin.data.Item;
+
+public abstract class AbstractHierarchicalContainerTestBase
+ extends AbstractContainerTestBase {
+
+ /**
+ * @param container
+ * The container to validate
+ * @param expectedFirstItemId
+ * Expected first item id
+ * @param expectedLastItemId
+ * Expected last item id
+ * @param itemIdInSet
+ * An item id that is in the container
+ * @param itemIdNotInSet
+ * An item id that is not in the container
+ * @param checkGetItemNull
+ * true if getItem() should return null for itemIdNotInSet, false
+ * to skip the check (container.containsId() is checked in any
+ * case)
+ * @param expectedSize
+ * Expected number of items in the container. Not related to
+ * hierarchy.
+ * @param expectedTraversalSize
+ * Expected number of items found when traversing from the roots
+ * down to all available nodes.
+ * @param expectedRootSize
+ * Expected number of root items
+ * @param rootsHaveChildren
+ * true if all roots have children, false otherwise (skips some
+ * asserts)
+ */
+ protected void validateHierarchicalContainer(Hierarchical container,
+ Object expectedFirstItemId, Object expectedLastItemId,
+ Object itemIdInSet, Object itemIdNotInSet, boolean checkGetItemNull,
+ int expectedSize, int expectedRootSize, boolean rootsHaveChildren) {
+
+ validateContainer(container, expectedFirstItemId, expectedLastItemId,
+ itemIdInSet, itemIdNotInSet, checkGetItemNull, expectedSize);
+
+ // rootItemIds
+ Collection<?> rootIds = container.rootItemIds();
+ assertEquals(expectedRootSize, rootIds.size());
+
+ for (Object rootId : rootIds) {
+ // All roots must be in container
+ assertTrue(container.containsId(rootId));
+
+ // All roots must have no parent
+ assertNull(container.getParent(rootId));
+
+ // all roots must be roots
+ assertTrue(container.isRoot(rootId));
+
+ if (rootsHaveChildren) {
+ // all roots have children allowed in this case
+ assertTrue(container.areChildrenAllowed(rootId));
+
+ // all roots have children in this case
+ Collection<?> children = container.getChildren(rootId);
+ assertNotNull(rootId + " should have children", children);
+ assertTrue(rootId + " should have children",
+ (children.size() > 0));
+ // getParent
+ for (Object childId : children) {
+ assertEquals(container.getParent(childId), rootId);
+ }
+
+ }
+ }
+
+ // isRoot should return false for unknown items
+ assertFalse(container.isRoot(itemIdNotInSet));
+
+ // hasChildren should return false for unknown items
+ assertFalse(container.hasChildren(itemIdNotInSet));
+
+ // areChildrenAllowed should return false for unknown items
+ assertFalse(container.areChildrenAllowed(itemIdNotInSet));
+
+ // removeItem of unknown items should return false
+ assertFalse(container.removeItem(itemIdNotInSet));
+
+ assertEquals(expectedSize, countNodes(container));
+
+ validateHierarchy(container);
+ }
+
+ private int countNodes(Hierarchical container) {
+ int totalNodes = 0;
+ for (Object rootId : container.rootItemIds()) {
+ totalNodes += countNodes(container, rootId);
+ }
+
+ return totalNodes;
+ }
+
+ private int countNodes(Hierarchical container, Object itemId) {
+ int nodes = 1; // This
+ Collection<?> children = container.getChildren(itemId);
+ if (children != null) {
+ for (Object id : children) {
+ nodes += countNodes(container, id);
+ }
+ }
+
+ return nodes;
+ }
+
+ private void validateHierarchy(Hierarchical container) {
+ for (Object rootId : container.rootItemIds()) {
+ validateHierarchy(container, rootId, null);
+ }
+ }
+
+ private void validateHierarchy(Hierarchical container, Object itemId,
+ Object parentId) {
+ Collection<?> children = container.getChildren(itemId);
+
+ // getParent
+ assertEquals(container.getParent(itemId), parentId);
+
+ if (!container.areChildrenAllowed(itemId)) {
+ // If no children is allowed the item should have no children
+ assertFalse(container.hasChildren(itemId));
+ assertTrue(children == null || children.size() == 0);
+
+ return;
+ }
+ if (children != null) {
+ for (Object id : children) {
+ validateHierarchy(container, id, itemId);
+ }
+ }
+ }
+
+ protected void testHierarchicalContainer(Container.Hierarchical container) {
+ initializeContainer(container);
+
+ int packages = 21 + 3;
+ int expectedSize = sampleData.length + packages;
+ validateHierarchicalContainer(container, "com",
+ "org.vaadin.test.LastClass",
+ "com.vaadin.server.ApplicationResource", "blah", true,
+ expectedSize, 2, true);
+
+ }
+
+ protected void testHierarchicalSorting(Container.Hierarchical container) {
+ Container.Sortable sortable = (Sortable) container;
+
+ initializeContainer(container);
+
+ // Must be able to sort based on PROP1 and PROP2 for this test
+ assertTrue(sortable.getSortableContainerPropertyIds()
+ .contains(FULLY_QUALIFIED_NAME));
+ assertTrue(sortable.getSortableContainerPropertyIds()
+ .contains(REVERSE_FULLY_QUALIFIED_NAME));
+
+ sortable.sort(new Object[] { FULLY_QUALIFIED_NAME },
+ new boolean[] { true });
+
+ int packages = 21 + 3;
+ int expectedSize = sampleData.length + packages;
+ validateHierarchicalContainer(container, "com",
+ "org.vaadin.test.LastClass",
+ "com.vaadin.server.ApplicationResource", "blah", true,
+ expectedSize, 2, true);
+
+ sortable.sort(new Object[] { REVERSE_FULLY_QUALIFIED_NAME },
+ new boolean[] { true });
+
+ validateHierarchicalContainer(container,
+ "com.vaadin.server.ApplicationPortlet2",
+ "com.vaadin.data.util.ObjectProperty",
+ "com.vaadin.server.ApplicationResource", "blah", true,
+ expectedSize, 2, true);
+
+ }
+
+ protected void initializeContainer(Container.Hierarchical container) {
+ container.removeAllItems();
+ Object[] propertyIds = container.getContainerPropertyIds().toArray();
+ for (Object propertyId : propertyIds) {
+ container.removeContainerProperty(propertyId);
+ }
+
+ container.addContainerProperty(FULLY_QUALIFIED_NAME, String.class, "");
+ container.addContainerProperty(SIMPLE_NAME, String.class, "");
+ container.addContainerProperty(REVERSE_FULLY_QUALIFIED_NAME,
+ String.class, null);
+ container.addContainerProperty(ID_NUMBER, Integer.class, null);
+
+ for (int i = 0; i < sampleData.length; i++) {
+ String id = sampleData[i];
+
+ // Add path as parent
+ String paths[] = id.split("\\.");
+ String path = paths[0];
+ // Adds "com" and other items multiple times so should return null
+ // for all but the first time
+ if (container.addItem(path) != null) {
+ assertTrue(container.setChildrenAllowed(path, false));
+ Item item = container.getItem(path);
+ item.getItemProperty(FULLY_QUALIFIED_NAME).setValue(path);
+ item.getItemProperty(SIMPLE_NAME).setValue(getSimpleName(path));
+ item.getItemProperty(REVERSE_FULLY_QUALIFIED_NAME)
+ .setValue(reverse(path));
+ item.getItemProperty(ID_NUMBER).setValue(1);
+ }
+ for (int j = 1; j < paths.length; j++) {
+ String parent = path;
+ path = path + "." + paths[j];
+
+ // Adds "com" and other items multiple times so should return
+ // null for all but the first time
+ if (container.addItem(path) != null) {
+ assertTrue(container.setChildrenAllowed(path, false));
+
+ Item item = container.getItem(path);
+ item.getItemProperty(FULLY_QUALIFIED_NAME).setValue(path);
+ item.getItemProperty(SIMPLE_NAME)
+ .setValue(getSimpleName(path));
+ item.getItemProperty(REVERSE_FULLY_QUALIFIED_NAME)
+ .setValue(reverse(path));
+ item.getItemProperty(ID_NUMBER).setValue(1);
+
+ }
+ assertTrue(container.setChildrenAllowed(parent, true));
+ assertTrue("Failed to set " + parent + " as parent for " + path,
+ container.setParent(path, parent));
+ }
+
+ Item item = container.getItem(id);
+ assertNotNull(item);
+ String parent = id.substring(0, id.lastIndexOf('.'));
+ assertTrue(container.setParent(id, parent));
+ item.getItemProperty(FULLY_QUALIFIED_NAME).setValue(sampleData[i]);
+ item.getItemProperty(SIMPLE_NAME)
+ .setValue(getSimpleName(sampleData[i]));
+ item.getItemProperty(REVERSE_FULLY_QUALIFIED_NAME)
+ .setValue(reverse(sampleData[i]));
+ item.getItemProperty(ID_NUMBER).setValue(i % 2);
+ }
+ }
+
+ protected void testRemoveHierarchicalWrapperSubtree(
+ Container.Hierarchical container) {
+ initializeContainer(container);
+
+ // remove root item
+ removeItemRecursively(container, "org");
+
+ int packages = 21 + 3 - 3;
+ int expectedSize = sampleData.length + packages - 1;
+
+ validateContainer(container, "com", "com.vaadin.util.SerializerHelper",
+ "com.vaadin.server.ApplicationResource", "blah", true,
+ expectedSize);
+
+ // rootItemIds
+ Collection<?> rootIds = container.rootItemIds();
+ assertEquals(1, rootIds.size());
+ }
+
+ private void removeItemRecursively(Container.Hierarchical container,
+ Object itemId) {
+ if (container instanceof ContainerHierarchicalWrapper) {
+ ((ContainerHierarchicalWrapper) container)
+ .removeItemRecursively("org");
+ } else {
+ HierarchicalContainer.removeItemRecursively(container, itemId);
+ }
+ }
+
+}
diff --git a/compatibility-server/src/test/java/com/vaadin/data/util/AbstractInMemoryContainerTestBase.java b/compatibility-server/src/test/java/com/vaadin/data/util/AbstractInMemoryContainerTestBase.java
new file mode 100644
index 0000000000..3858504bc7
--- /dev/null
+++ b/compatibility-server/src/test/java/com/vaadin/data/util/AbstractInMemoryContainerTestBase.java
@@ -0,0 +1,6 @@
+package com.vaadin.data.util;
+
+public abstract class AbstractInMemoryContainerTestBase
+ extends AbstractContainerTestBase {
+
+}
diff --git a/compatibility-server/src/test/java/com/vaadin/data/util/BeanContainerTest.java b/compatibility-server/src/test/java/com/vaadin/data/util/BeanContainerTest.java
new file mode 100644
index 0000000000..bdf6ba1958
--- /dev/null
+++ b/compatibility-server/src/test/java/com/vaadin/data/util/BeanContainerTest.java
@@ -0,0 +1,516 @@
+package com.vaadin.data.util;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.fail;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Map.Entry;
+
+import org.junit.Assert;
+import org.junit.Before;
+import org.junit.Test;
+
+import com.vaadin.data.Container;
+import com.vaadin.data.Item;
+import com.vaadin.data.util.AbstractBeanContainer.BeanIdResolver;
+
+public class BeanContainerTest extends AbstractBeanContainerTestBase {
+
+ protected static class PersonNameResolver
+ implements BeanIdResolver<String, Person> {
+
+ @Override
+ public String getIdForBean(Person bean) {
+ return bean != null ? bean.getName() : null;
+ }
+
+ }
+
+ protected static class NullResolver
+ implements BeanIdResolver<String, Person> {
+
+ @Override
+ public String getIdForBean(Person bean) {
+ return null;
+ }
+
+ }
+
+ private Map<String, ClassName> nameToBean = new LinkedHashMap<String, ClassName>();
+
+ private BeanContainer<String, ClassName> getContainer() {
+ return new BeanContainer<String, ClassName>(ClassName.class);
+ }
+
+ @Before
+ public void setUp() {
+ nameToBean.clear();
+
+ for (int i = 0; i < sampleData.length; i++) {
+ ClassName className = new ClassName(sampleData[i], i);
+ nameToBean.put(sampleData[i], className);
+ }
+ }
+
+ @Override
+ @SuppressWarnings("unchecked")
+ protected void initializeContainer(Container container) {
+ BeanContainer<String, ClassName> beanItemContainer = (BeanContainer<String, ClassName>) container;
+
+ beanItemContainer.removeAllItems();
+
+ for (Entry<String, ClassName> entry : nameToBean.entrySet()) {
+ beanItemContainer.addItem(entry.getKey(), entry.getValue());
+ }
+ }
+
+ @Override
+ protected boolean isFilteredOutItemNull() {
+ return false;
+ }
+
+ @Test
+ public void testGetType_existingProperty_typeReturned() {
+ BeanContainer<String, ClassName> container = getContainer();
+ Assert.assertEquals(
+ "Unexpected type is returned for property 'simpleName'",
+ String.class, container.getType("simpleName"));
+ }
+
+ @Test
+ public void testGetType_notExistingProperty_nullReturned() {
+ BeanContainer<String, ClassName> container = getContainer();
+ Assert.assertNull("Not null type is returned for property ''",
+ container.getType(""));
+ }
+
+ @Test
+ public void testBasicOperations() {
+ testBasicContainerOperations(getContainer());
+ }
+
+ @Test
+ public void testFiltering() {
+ testContainerFiltering(getContainer());
+ }
+
+ @Test
+ public void testSorting() {
+ testContainerSorting(getContainer());
+ }
+
+ @Test
+ public void testSortingAndFiltering() {
+ testContainerSortingAndFiltering(getContainer());
+ }
+
+ // duplicated from parent class and modified - adding items to
+ // BeanContainer differs from other containers
+ @Test
+ public void testContainerOrdered() {
+ BeanContainer<String, String> container = new BeanContainer<String, String>(
+ String.class);
+
+ String id = "test1";
+
+ Item item = container.addItem(id, "value");
+ assertNotNull(item);
+
+ assertEquals(id, container.firstItemId());
+ assertEquals(id, container.lastItemId());
+
+ // isFirstId
+ assertTrue(container.isFirstId(id));
+ assertTrue(container.isFirstId(container.firstItemId()));
+ // isLastId
+ assertTrue(container.isLastId(id));
+ assertTrue(container.isLastId(container.lastItemId()));
+
+ // Add a new item before the first
+ // addItemAfter
+ String newFirstId = "newFirst";
+ item = container.addItemAfter(null, newFirstId, "newFirstValue");
+ assertNotNull(item);
+ assertNotNull(container.getItem(newFirstId));
+
+ // isFirstId
+ assertTrue(container.isFirstId(newFirstId));
+ assertTrue(container.isFirstId(container.firstItemId()));
+ // isLastId
+ assertTrue(container.isLastId(id));
+ assertTrue(container.isLastId(container.lastItemId()));
+
+ // nextItemId
+ assertEquals(id, container.nextItemId(newFirstId));
+ assertNull(container.nextItemId(id));
+ assertNull(container.nextItemId("not-in-container"));
+
+ // prevItemId
+ assertEquals(newFirstId, container.prevItemId(id));
+ assertNull(container.prevItemId(newFirstId));
+ assertNull(container.prevItemId("not-in-container"));
+
+ // addItemAfter(IDTYPE, IDTYPE, BT)
+ String newSecondItemId = "newSecond";
+ item = container.addItemAfter(newFirstId, newSecondItemId,
+ "newSecondValue");
+ // order is now: newFirstId, newSecondItemId, id
+ assertNotNull(item);
+ assertNotNull(container.getItem(newSecondItemId));
+ assertEquals(id, container.nextItemId(newSecondItemId));
+ assertEquals(newFirstId, container.prevItemId(newSecondItemId));
+
+ // addItemAfter(IDTYPE, IDTYPE, BT)
+ String fourthId = "id of the fourth item";
+ Item fourth = container.addItemAfter(newFirstId, fourthId,
+ "fourthValue");
+ // order is now: newFirstId, fourthId, newSecondItemId, id
+ assertNotNull(fourth);
+ assertEquals(fourth, container.getItem(fourthId));
+ assertEquals(newSecondItemId, container.nextItemId(fourthId));
+ assertEquals(newFirstId, container.prevItemId(fourthId));
+
+ // addItemAfter(IDTYPE, IDTYPE, BT)
+ String fifthId = "fifth";
+ Item fifth = container.addItemAfter(null, fifthId, "fifthValue");
+ // order is now: fifthId, newFirstId, fourthId, newSecondItemId, id
+ assertNotNull(fifth);
+ assertEquals(fifth, container.getItem(fifthId));
+ assertEquals(newFirstId, container.nextItemId(fifthId));
+ assertNull(container.prevItemId(fifthId));
+
+ }
+
+ // TODO test Container.Indexed interface operation - testContainerIndexed()?
+
+ @Test
+ public void testAddItemAt() {
+ BeanContainer<String, String> container = new BeanContainer<String, String>(
+ String.class);
+
+ container.addItem("id1", "value1");
+ // id1
+ container.addItemAt(0, "id2", "value2");
+ // id2, id1
+ container.addItemAt(1, "id3", "value3");
+ // id2, id3, id1
+ container.addItemAt(container.size(), "id4", "value4");
+ // id2, id3, id1, id4
+
+ assertNull(container.addItemAt(-1, "id5", "value5"));
+ assertNull(container.addItemAt(container.size() + 1, "id6", "value6"));
+
+ assertEquals(4, container.size());
+ assertEquals("id2", container.getIdByIndex(0));
+ assertEquals("id3", container.getIdByIndex(1));
+ assertEquals("id1", container.getIdByIndex(2));
+ assertEquals("id4", container.getIdByIndex(3));
+ }
+
+ @Test
+ public void testUnsupportedMethods() {
+ BeanContainer<String, Person> container = new BeanContainer<String, Person>(
+ Person.class);
+ container.addItem("John", new Person("John"));
+
+ try {
+ container.addItem();
+ Assert.fail();
+ } catch (UnsupportedOperationException e) {
+ // should get exception
+ }
+
+ try {
+ container.addItem(null);
+ Assert.fail();
+ } catch (UnsupportedOperationException e) {
+ // should get exception
+ }
+
+ try {
+ container.addItemAfter(null, null);
+ Assert.fail();
+ } catch (UnsupportedOperationException e) {
+ // should get exception
+ }
+
+ try {
+ container.addItemAfter(new Person("Jane"));
+ Assert.fail();
+ } catch (UnsupportedOperationException e) {
+ // should get exception
+ }
+
+ try {
+ container.addItemAt(0);
+ Assert.fail();
+ } catch (UnsupportedOperationException e) {
+ // should get exception
+ }
+
+ try {
+ container.addItemAt(0, new Person("Jane"));
+ Assert.fail();
+ } catch (UnsupportedOperationException e) {
+ // should get exception
+ }
+
+ try {
+ container.addContainerProperty("lastName", String.class, "");
+ Assert.fail();
+ } catch (UnsupportedOperationException e) {
+ // should get exception
+ }
+
+ assertEquals(1, container.size());
+ }
+
+ @Test
+ public void testRemoveContainerProperty() {
+ BeanContainer<String, Person> container = new BeanContainer<String, Person>(
+ Person.class);
+ container.setBeanIdResolver(new PersonNameResolver());
+ container.addBean(new Person("John"));
+
+ Assert.assertEquals("John",
+ container.getContainerProperty("John", "name").getValue());
+ Assert.assertTrue(container.removeContainerProperty("name"));
+ Assert.assertNull(container.getContainerProperty("John", "name"));
+
+ Assert.assertNotNull(container.getItem("John"));
+ // property removed also from item
+ Assert.assertNull(container.getItem("John").getItemProperty("name"));
+ }
+
+ @Test
+ public void testAddNullBeans() {
+ BeanContainer<String, Person> container = new BeanContainer<String, Person>(
+ Person.class);
+
+ assertNull(container.addItem("id1", null));
+ assertNull(container.addItemAfter(null, "id2", null));
+ assertNull(container.addItemAt(0, "id3", null));
+
+ assertEquals(0, container.size());
+ }
+
+ @Test
+ public void testAddNullId() {
+ BeanContainer<String, Person> container = new BeanContainer<String, Person>(
+ Person.class);
+
+ Person john = new Person("John");
+
+ assertNull(container.addItem(null, john));
+ assertNull(container.addItemAfter(null, null, john));
+ assertNull(container.addItemAt(0, null, john));
+
+ assertEquals(0, container.size());
+ }
+
+ @Test
+ public void testEmptyContainer() {
+ BeanContainer<String, Person> container = new BeanContainer<String, Person>(
+ Person.class);
+
+ assertNull(container.firstItemId());
+ assertNull(container.lastItemId());
+
+ assertEquals(0, container.size());
+
+ // could test more about empty container
+ }
+
+ @Test
+ public void testAddBeanWithoutResolver() {
+ BeanContainer<String, Person> container = new BeanContainer<String, Person>(
+ Person.class);
+
+ try {
+ container.addBean(new Person("John"));
+ Assert.fail();
+ } catch (IllegalStateException e) {
+ // should get exception
+ }
+ try {
+ container.addBeanAfter(null, new Person("Jane"));
+ Assert.fail();
+ } catch (IllegalStateException e) {
+ // should get exception
+ }
+ try {
+ container.addBeanAt(0, new Person("Jack"));
+ Assert.fail();
+ } catch (IllegalStateException e) {
+ // should get exception
+ }
+ try {
+ container
+ .addAll(Arrays.asList(new Person[] { new Person("Jack") }));
+ Assert.fail();
+ } catch (IllegalStateException e) {
+ // should get exception
+ }
+
+ assertEquals(0, container.size());
+ }
+
+ @Test
+ public void testAddAllWithNullItemId() {
+ BeanContainer<String, Person> container = new BeanContainer<String, Person>(
+ Person.class);
+ // resolver that returns null as item id
+ container.setBeanIdResolver(
+ new BeanIdResolver<String, AbstractBeanContainerTestBase.Person>() {
+
+ @Override
+ public String getIdForBean(Person bean) {
+ return bean.getName();
+ }
+ });
+
+ List<Person> persons = new ArrayList<Person>();
+ persons.add(new Person("John"));
+ persons.add(new Person("Marc"));
+ persons.add(new Person(null));
+ persons.add(new Person("foo"));
+
+ try {
+ container.addAll(persons);
+ fail();
+ } catch (IllegalArgumentException e) {
+ // should get exception
+ }
+
+ container.removeAllItems();
+ persons.remove(2);
+ container.addAll(persons);
+ assertEquals(3, container.size());
+ }
+
+ @Test
+ public void testAddBeanWithNullResolver() {
+ BeanContainer<String, Person> container = new BeanContainer<String, Person>(
+ Person.class);
+ // resolver that returns null as item id
+ container.setBeanIdResolver(new NullResolver());
+
+ try {
+ container.addBean(new Person("John"));
+ Assert.fail();
+ } catch (IllegalArgumentException e) {
+ // should get exception
+ }
+ try {
+ container.addBeanAfter(null, new Person("Jane"));
+ Assert.fail();
+ } catch (IllegalArgumentException e) {
+ // should get exception
+ }
+ try {
+ container.addBeanAt(0, new Person("Jack"));
+ Assert.fail();
+ } catch (IllegalArgumentException e) {
+ // should get exception
+ }
+
+ assertEquals(0, container.size());
+ }
+
+ @Test
+ public void testAddBeanWithResolver() {
+ BeanContainer<String, Person> container = new BeanContainer<String, Person>(
+ Person.class);
+ container.setBeanIdResolver(new PersonNameResolver());
+
+ assertNotNull(container.addBean(new Person("John")));
+ assertNotNull(container.addBeanAfter(null, new Person("Jane")));
+ assertNotNull(container.addBeanAt(0, new Person("Jack")));
+
+ container.addAll(Arrays.asList(
+ new Person[] { new Person("Jill"), new Person("Joe") }));
+
+ assertTrue(container.containsId("John"));
+ assertTrue(container.containsId("Jane"));
+ assertTrue(container.containsId("Jack"));
+ assertTrue(container.containsId("Jill"));
+ assertTrue(container.containsId("Joe"));
+ assertEquals(3, container.indexOfId("Jill"));
+ assertEquals(4, container.indexOfId("Joe"));
+ assertEquals(5, container.size());
+ }
+
+ @Test
+ public void testAddNullBeansWithResolver() {
+ BeanContainer<String, Person> container = new BeanContainer<String, Person>(
+ Person.class);
+ container.setBeanIdResolver(new PersonNameResolver());
+
+ assertNull(container.addBean(null));
+ assertNull(container.addBeanAfter(null, null));
+ assertNull(container.addBeanAt(0, null));
+
+ assertEquals(0, container.size());
+ }
+
+ @Test
+ public void testAddBeanWithPropertyResolver() {
+ BeanContainer<String, Person> container = new BeanContainer<String, Person>(
+ Person.class);
+ container.setBeanIdProperty("name");
+
+ assertNotNull(container.addBean(new Person("John")));
+ assertNotNull(container.addBeanAfter(null, new Person("Jane")));
+ assertNotNull(container.addBeanAt(0, new Person("Jack")));
+
+ container.addAll(Arrays.asList(
+ new Person[] { new Person("Jill"), new Person("Joe") }));
+
+ assertTrue(container.containsId("John"));
+ assertTrue(container.containsId("Jane"));
+ assertTrue(container.containsId("Jack"));
+ assertTrue(container.containsId("Jill"));
+ assertTrue(container.containsId("Joe"));
+ assertEquals(3, container.indexOfId("Jill"));
+ assertEquals(4, container.indexOfId("Joe"));
+ assertEquals(5, container.size());
+ }
+
+ @Test
+ public void testAddNestedContainerProperty() {
+ BeanContainer<String, NestedMethodPropertyTest.Person> container = new BeanContainer<String, NestedMethodPropertyTest.Person>(
+ NestedMethodPropertyTest.Person.class);
+ container.setBeanIdProperty("name");
+
+ container.addBean(new NestedMethodPropertyTest.Person("John",
+ new NestedMethodPropertyTest.Address("Ruukinkatu 2-4", 20540)));
+
+ assertTrue(container.addNestedContainerProperty("address.street"));
+ assertEquals("Ruukinkatu 2-4", container
+ .getContainerProperty("John", "address.street").getValue());
+ }
+
+ @Test
+ public void testNestedContainerPropertyWithNullBean() {
+ BeanContainer<String, NestedMethodPropertyTest.Person> container = new BeanContainer<String, NestedMethodPropertyTest.Person>(
+ NestedMethodPropertyTest.Person.class);
+ container.setBeanIdProperty("name");
+
+ container.addBean(new NestedMethodPropertyTest.Person("John", null));
+ assertTrue(container
+ .addNestedContainerProperty("address.postalCodeObject"));
+ assertTrue(container.addNestedContainerProperty("address.street"));
+ // the nested properties added with allowNullBean setting should return
+ // null
+ assertNull(container.getContainerProperty("John", "address.street")
+ .getValue());
+ }
+
+}
diff --git a/compatibility-server/src/test/java/com/vaadin/data/util/BeanItemContainerGenerator.java b/compatibility-server/src/test/java/com/vaadin/data/util/BeanItemContainerGenerator.java
new file mode 100644
index 0000000000..a5bdcc7cf9
--- /dev/null
+++ b/compatibility-server/src/test/java/com/vaadin/data/util/BeanItemContainerGenerator.java
@@ -0,0 +1,150 @@
+package com.vaadin.data.util;
+
+import java.util.Date;
+import java.util.concurrent.atomic.AtomicLong;
+
+public class BeanItemContainerGenerator {
+
+ public static class PortableRandom {
+ private final static long multiplier = 0x5DEECE66DL;
+ private final static long addend = 0xBL;
+ private final static long mask = (1L << 48) - 1;
+ private AtomicLong seed;
+
+ public PortableRandom(long seed) {
+ this.seed = new AtomicLong(0L);
+ setSeed(seed);
+ }
+
+ synchronized public void setSeed(long seed) {
+ seed = (seed ^ multiplier) & mask;
+ this.seed.set(seed);
+ }
+
+ public int nextInt(int n) {
+ if (n <= 0) {
+ throw new IllegalArgumentException("n must be positive");
+ }
+
+ if ((n & -n) == n) {
+ return (int) ((n * (long) next(31)) >> 31);
+ }
+
+ int bits, val;
+ do {
+ bits = next(31);
+ val = bits % n;
+ } while (bits - val + (n - 1) < 0);
+ return val;
+ }
+
+ protected int next(int bits) {
+ long oldseed, nextseed;
+ AtomicLong seed = this.seed;
+ do {
+ oldseed = seed.get();
+ nextseed = (oldseed * multiplier + addend) & mask;
+ } while (!seed.compareAndSet(oldseed, nextseed));
+ return (int) (nextseed >>> (48 - bits));
+ }
+
+ public boolean nextBoolean() {
+ return next(1) != 0;
+ }
+
+ }
+
+ public static BeanItemContainer<TestBean> createContainer(int size) {
+ return createContainer(size, new Date().getTime());
+ }
+
+ public static BeanItemContainer<TestBean> createContainer(int size,
+ long seed) {
+
+ BeanItemContainer<TestBean> container = new BeanItemContainer<TestBean>(
+ TestBean.class);
+ PortableRandom r = new PortableRandom(seed);
+ for (int i = 0; i < size; i++) {
+ container.addBean(new TestBean(r));
+ }
+
+ return container;
+
+ }
+
+ public static class TestBean {
+ private String name, address, city, country;
+ private int age, shoesize;
+
+ public int getAge() {
+ return age;
+ }
+
+ public void setAge(int age) {
+ this.age = age;
+ }
+
+ public int getShoesize() {
+ return shoesize;
+ }
+
+ public void setShoesize(int shoesize) {
+ this.shoesize = shoesize;
+ }
+
+ public TestBean(PortableRandom r) {
+ age = r.nextInt(100) + 5;
+ shoesize = r.nextInt(10) + 35;
+ name = createRandomString(r, r.nextInt(5) + 5);
+ address = createRandomString(r, r.nextInt(15) + 5) + " "
+ + r.nextInt(100) + 1;
+ city = createRandomString(r, r.nextInt(7) + 3);
+ if (r.nextBoolean()) {
+ country = createRandomString(r, r.nextInt(4) + 4);
+ }
+ }
+
+ public String getName() {
+ return name;
+ }
+
+ public void setName(String name) {
+ this.name = name;
+ }
+
+ public String getAddress() {
+ return address;
+ }
+
+ public void setAddress(String address) {
+ this.address = address;
+ }
+
+ public String getCity() {
+ return city;
+ }
+
+ public void setCity(String city) {
+ this.city = city;
+ }
+
+ public String getCountry() {
+ return country;
+ }
+
+ public void setCountry(String country) {
+ this.country = country;
+ }
+
+ }
+
+ public static String createRandomString(PortableRandom r, int len) {
+ StringBuilder b = new StringBuilder();
+ for (int i = 0; i < len; i++) {
+ b.append((char) (r.nextInt('z' - 'a') + 'a'));
+ }
+
+ return b.toString();
+ }
+
+}
diff --git a/compatibility-server/src/test/java/com/vaadin/data/util/BeanItemContainerSortTest.java b/compatibility-server/src/test/java/com/vaadin/data/util/BeanItemContainerSortTest.java
new file mode 100644
index 0000000000..4f4e35258f
--- /dev/null
+++ b/compatibility-server/src/test/java/com/vaadin/data/util/BeanItemContainerSortTest.java
@@ -0,0 +1,166 @@
+package com.vaadin.data.util;
+
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Set;
+
+import org.junit.Assert;
+import org.junit.Test;
+
+public class BeanItemContainerSortTest {
+ public class Person {
+ private String name;
+
+ public int getAge() {
+ return age;
+ }
+
+ public void setAge(int age) {
+ this.age = age;
+ }
+
+ private int age;
+
+ public void setName(String name) {
+ this.name = name;
+ }
+
+ public String getName() {
+ return name;
+ }
+ }
+
+ public class Parent extends Person {
+ private Set<Person> children = new HashSet<Person>();
+
+ public void setChildren(Set<Person> children) {
+ this.children = children;
+ }
+
+ public Set<Person> getChildren() {
+ return children;
+ }
+ }
+
+ String[] names = new String[] { "Antti", "Ville", "Sirkka", "Jaakko",
+ "Pekka", "John" };
+ int[] ages = new int[] { 10, 20, 50, 12, 64, 67 };
+ String[] sortedByAge = new String[] { names[0], names[3], names[1],
+ names[2], names[4], names[5] };
+
+ public BeanItemContainer<Person> getContainer() {
+ BeanItemContainer<Person> bc = new BeanItemContainer<Person>(
+ Person.class);
+ for (int i = 0; i < names.length; i++) {
+ Person p = new Person();
+ p.setName(names[i]);
+ p.setAge(ages[i]);
+ bc.addBean(p);
+ }
+ return bc;
+
+ }
+
+ public BeanItemContainer<Parent> getParentContainer() {
+ BeanItemContainer<Parent> bc = new BeanItemContainer<Parent>(
+ Parent.class);
+ for (int i = 0; i < names.length; i++) {
+ Parent p = new Parent();
+ p.setName(names[i]);
+ p.setAge(ages[i]);
+ bc.addBean(p);
+ }
+ return bc;
+ }
+
+ @Test
+ public void testSort() {
+ testSort(true);
+ }
+
+ public void testSort(boolean b) {
+ BeanItemContainer<Person> container = getContainer();
+ container.sort(new Object[] { "name" }, new boolean[] { b });
+
+ List<String> asList = Arrays.asList(names);
+ Collections.sort(asList);
+ if (!b) {
+ Collections.reverse(asList);
+ }
+
+ int i = 0;
+ for (String string : asList) {
+ Person idByIndex = container.getIdByIndex(i++);
+ Assert.assertTrue(container.containsId(idByIndex));
+ Assert.assertEquals(string, idByIndex.getName());
+ }
+ }
+
+ @Test
+ public void testReverseSort() {
+ testSort(false);
+ }
+
+ @Test
+ public void primitiveSorting() {
+ BeanItemContainer<Person> container = getContainer();
+ container.sort(new Object[] { "age" }, new boolean[] { true });
+
+ int i = 0;
+ for (String string : sortedByAge) {
+ Person idByIndex = container.getIdByIndex(i++);
+ Assert.assertTrue(container.containsId(idByIndex));
+ Assert.assertEquals(string, idByIndex.getName());
+ }
+ }
+
+ @Test
+ public void customSorting() {
+ BeanItemContainer<Person> container = getContainer();
+
+ // custom sorter using the reverse order
+ container.setItemSorter(new DefaultItemSorter() {
+ @Override
+ public int compare(Object o1, Object o2) {
+ return -super.compare(o1, o2);
+ }
+ });
+
+ container.sort(new Object[] { "age" }, new boolean[] { true });
+
+ int i = container.size() - 1;
+ for (String string : sortedByAge) {
+ Person idByIndex = container.getIdByIndex(i--);
+ Assert.assertTrue(container.containsId(idByIndex));
+ Assert.assertEquals(string, idByIndex.getName());
+ }
+ }
+
+ @Test
+ public void testGetSortableProperties() {
+ BeanItemContainer<Person> container = getContainer();
+
+ Collection<?> sortablePropertyIds = container
+ .getSortableContainerPropertyIds();
+ Assert.assertEquals(2, sortablePropertyIds.size());
+ Assert.assertTrue(sortablePropertyIds.contains("name"));
+ Assert.assertTrue(sortablePropertyIds.contains("age"));
+ }
+
+ @Test
+ public void testGetNonSortableProperties() {
+ BeanItemContainer<Parent> container = getParentContainer();
+
+ Assert.assertEquals(3, container.getContainerPropertyIds().size());
+
+ Collection<?> sortablePropertyIds = container
+ .getSortableContainerPropertyIds();
+ Assert.assertEquals(2, sortablePropertyIds.size());
+ Assert.assertTrue(sortablePropertyIds.contains("name"));
+ Assert.assertTrue(sortablePropertyIds.contains("age"));
+ }
+
+}
diff --git a/compatibility-server/src/test/java/com/vaadin/data/util/BeanItemContainerTest.java b/compatibility-server/src/test/java/com/vaadin/data/util/BeanItemContainerTest.java
new file mode 100644
index 0000000000..19b0835fd6
--- /dev/null
+++ b/compatibility-server/src/test/java/com/vaadin/data/util/BeanItemContainerTest.java
@@ -0,0 +1,997 @@
+package com.vaadin.data.util;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertSame;
+import static org.junit.Assert.assertTrue;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.Iterator;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+
+import org.easymock.Capture;
+import org.easymock.EasyMock;
+import org.junit.Assert;
+import org.junit.Before;
+import org.junit.Test;
+
+import com.vaadin.data.Container;
+import com.vaadin.data.Container.Indexed.ItemAddEvent;
+import com.vaadin.data.Container.Indexed.ItemRemoveEvent;
+import com.vaadin.data.Container.ItemSetChangeListener;
+import com.vaadin.data.Item;
+import com.vaadin.data.util.NestedMethodPropertyTest.Address;
+import com.vaadin.data.util.filter.Compare;
+
+/**
+ * Test basic functionality of BeanItemContainer.
+ *
+ * Most sorting related tests are in {@link BeanItemContainerSortTest}.
+ */
+public class BeanItemContainerTest extends AbstractBeanContainerTestBase {
+
+ // basics from the common container test
+
+ private Map<String, ClassName> nameToBean = new LinkedHashMap<String, ClassName>();
+
+ private BeanItemContainer<ClassName> getContainer() {
+ return new BeanItemContainer<ClassName>(ClassName.class);
+ }
+
+ @Before
+ public void setUp() {
+ nameToBean.clear();
+
+ for (int i = 0; i < sampleData.length; i++) {
+ ClassName className = new ClassName(sampleData[i], i);
+ nameToBean.put(sampleData[i], className);
+ }
+ }
+
+ @Override
+ @SuppressWarnings("unchecked")
+ protected void initializeContainer(Container container) {
+ BeanItemContainer<ClassName> beanItemContainer = (BeanItemContainer<ClassName>) container;
+
+ beanItemContainer.removeAllItems();
+
+ Iterator<ClassName> it = nameToBean.values().iterator();
+ while (it.hasNext()) {
+ beanItemContainer.addBean(it.next());
+ }
+ }
+
+ @Override
+ protected void validateContainer(Container container,
+ Object expectedFirstItemId, Object expectedLastItemId,
+ Object itemIdInSet, Object itemIdNotInSet, boolean checkGetItemNull,
+ int expectedSize) {
+ Object notInSet = nameToBean.get(itemIdNotInSet);
+ if (notInSet == null && itemIdNotInSet != null) {
+ notInSet = new ClassName(String.valueOf(itemIdNotInSet), 9999);
+ }
+ super.validateContainer(container, nameToBean.get(expectedFirstItemId),
+ nameToBean.get(expectedLastItemId), nameToBean.get(itemIdInSet),
+ notInSet, checkGetItemNull, expectedSize);
+ }
+
+ @Override
+ protected boolean isFilteredOutItemNull() {
+ return false;
+ }
+
+ @Test
+ public void testGetType_existingProperty_typeReturned() {
+ BeanItemContainer<ClassName> container = getContainer();
+ Assert.assertEquals(
+ "Unexpected type is returned for property 'simpleName'",
+ String.class, container.getType("simpleName"));
+ }
+
+ @Test
+ public void testGetType_notExistingProperty_nullReturned() {
+ BeanItemContainer<ClassName> container = getContainer();
+ Assert.assertNull("Not null type is returned for property ''",
+ container.getType(""));
+ }
+
+ @Test
+ public void testBasicOperations() {
+ testBasicContainerOperations(getContainer());
+ }
+
+ @Test
+ public void testFiltering() {
+ testContainerFiltering(getContainer());
+ }
+
+ @Test
+ public void testSorting() {
+ testContainerSorting(getContainer());
+ }
+
+ @Test
+ public void testSortingAndFiltering() {
+ testContainerSortingAndFiltering(getContainer());
+ }
+
+ // duplicated from parent class and modified - adding items to
+ // BeanItemContainer differs from other containers
+ @Test
+ public void testContainerOrdered() {
+ BeanItemContainer<String> container = new BeanItemContainer<String>(
+ String.class);
+
+ String id = "test1";
+
+ Item item = container.addBean(id);
+ assertNotNull(item);
+
+ assertEquals(id, container.firstItemId());
+ assertEquals(id, container.lastItemId());
+
+ // isFirstId
+ assertTrue(container.isFirstId(id));
+ assertTrue(container.isFirstId(container.firstItemId()));
+ // isLastId
+ assertTrue(container.isLastId(id));
+ assertTrue(container.isLastId(container.lastItemId()));
+
+ // Add a new item before the first
+ // addItemAfter
+ String newFirstId = "newFirst";
+ item = container.addItemAfter(null, newFirstId);
+ assertNotNull(item);
+ assertNotNull(container.getItem(newFirstId));
+
+ // isFirstId
+ assertTrue(container.isFirstId(newFirstId));
+ assertTrue(container.isFirstId(container.firstItemId()));
+ // isLastId
+ assertTrue(container.isLastId(id));
+ assertTrue(container.isLastId(container.lastItemId()));
+
+ // nextItemId
+ assertEquals(id, container.nextItemId(newFirstId));
+ assertNull(container.nextItemId(id));
+ assertNull(container.nextItemId("not-in-container"));
+
+ // prevItemId
+ assertEquals(newFirstId, container.prevItemId(id));
+ assertNull(container.prevItemId(newFirstId));
+ assertNull(container.prevItemId("not-in-container"));
+
+ // addItemAfter(Object)
+ String newSecondItemId = "newSecond";
+ item = container.addItemAfter(newFirstId, newSecondItemId);
+ // order is now: newFirstId, newSecondItemId, id
+ assertNotNull(item);
+ assertNotNull(container.getItem(newSecondItemId));
+ assertEquals(id, container.nextItemId(newSecondItemId));
+ assertEquals(newFirstId, container.prevItemId(newSecondItemId));
+
+ // addItemAfter(Object,Object)
+ String fourthId = "id of the fourth item";
+ Item fourth = container.addItemAfter(newFirstId, fourthId);
+ // order is now: newFirstId, fourthId, newSecondItemId, id
+ assertNotNull(fourth);
+ assertEquals(fourth, container.getItem(fourthId));
+ assertEquals(newSecondItemId, container.nextItemId(fourthId));
+ assertEquals(newFirstId, container.prevItemId(fourthId));
+
+ // addItemAfter(Object,Object)
+ Object fifthId = "fifth";
+ Item fifth = container.addItemAfter(null, fifthId);
+ // order is now: fifthId, newFirstId, fourthId, newSecondItemId, id
+ assertNotNull(fifth);
+ assertEquals(fifth, container.getItem(fifthId));
+ assertEquals(newFirstId, container.nextItemId(fifthId));
+ assertNull(container.prevItemId(fifthId));
+
+ }
+
+ @Test
+ public void testContainerIndexed() {
+ testContainerIndexed(getContainer(), nameToBean.get(sampleData[2]), 2,
+ false, new ClassName("org.vaadin.test.Test", 8888), true);
+ }
+
+ @SuppressWarnings("deprecation")
+ @Test
+ public void testCollectionConstructors() {
+ List<ClassName> classNames = new ArrayList<ClassName>();
+ classNames.add(new ClassName("a.b.c.Def", 1));
+ classNames.add(new ClassName("a.b.c.Fed", 2));
+ classNames.add(new ClassName("b.c.d.Def", 3));
+
+ // note that this constructor is problematic, users should use the
+ // version that
+ // takes the bean class as a parameter
+ BeanItemContainer<ClassName> container = new BeanItemContainer<ClassName>(
+ classNames);
+
+ Assert.assertEquals(3, container.size());
+ Assert.assertEquals(classNames.get(0), container.firstItemId());
+ Assert.assertEquals(classNames.get(1), container.getIdByIndex(1));
+ Assert.assertEquals(classNames.get(2), container.lastItemId());
+
+ BeanItemContainer<ClassName> container2 = new BeanItemContainer<ClassName>(
+ ClassName.class, classNames);
+
+ Assert.assertEquals(3, container2.size());
+ Assert.assertEquals(classNames.get(0), container2.firstItemId());
+ Assert.assertEquals(classNames.get(1), container2.getIdByIndex(1));
+ Assert.assertEquals(classNames.get(2), container2.lastItemId());
+ }
+
+ // this only applies to the collection constructor with no type parameter
+ @SuppressWarnings("deprecation")
+ @Test
+ public void testEmptyCollectionConstructor() {
+ try {
+ new BeanItemContainer<ClassName>((Collection<ClassName>) null);
+ Assert.fail(
+ "Initializing BeanItemContainer from a null collection should not work!");
+ } catch (IllegalArgumentException e) {
+ // success
+ }
+ try {
+ new BeanItemContainer<ClassName>(new ArrayList<ClassName>());
+ Assert.fail(
+ "Initializing BeanItemContainer from an empty collection should not work!");
+ } catch (IllegalArgumentException e) {
+ // success
+ }
+ }
+
+ @Test
+ public void testItemSetChangeListeners() {
+ BeanItemContainer<ClassName> container = getContainer();
+ ItemSetChangeCounter counter = new ItemSetChangeCounter();
+ container.addListener(counter);
+
+ ClassName cn1 = new ClassName("com.example.Test", 1111);
+ ClassName cn2 = new ClassName("com.example.Test2", 2222);
+
+ initializeContainer(container);
+ counter.reset();
+ container.addBean(cn1);
+ counter.assertOnce();
+
+ initializeContainer(container);
+ counter.reset();
+ container.addItem(cn1);
+ counter.assertOnce();
+ // no notification if already in container
+ container.addItem(cn1);
+ counter.assertNone();
+ container.addItem(cn2);
+ counter.assertOnce();
+
+ initializeContainer(container);
+ counter.reset();
+ container.addItemAfter(null, cn1);
+ counter.assertOnce();
+ Assert.assertEquals("com.example.Test",
+ container.getContainerProperty(container.firstItemId(),
+ FULLY_QUALIFIED_NAME).getValue());
+
+ initializeContainer(container);
+ counter.reset();
+ container.addItemAfter(container.firstItemId(), cn1);
+ counter.assertOnce();
+ Assert.assertEquals("com.example.Test",
+ container.getContainerProperty(container.getIdByIndex(1),
+ FULLY_QUALIFIED_NAME).getValue());
+
+ initializeContainer(container);
+ counter.reset();
+ container.addItemAfter(container.lastItemId(), cn1);
+ counter.assertOnce();
+ Assert.assertEquals("com.example.Test",
+ container.getContainerProperty(container.lastItemId(),
+ FULLY_QUALIFIED_NAME).getValue());
+
+ initializeContainer(container);
+ counter.reset();
+ container.addItemAt(0, cn1);
+ counter.assertOnce();
+ Assert.assertEquals("com.example.Test",
+ container.getContainerProperty(container.firstItemId(),
+ FULLY_QUALIFIED_NAME).getValue());
+
+ initializeContainer(container);
+ counter.reset();
+ container.addItemAt(1, cn1);
+ counter.assertOnce();
+ Assert.assertEquals("com.example.Test",
+ container.getContainerProperty(container.getIdByIndex(1),
+ FULLY_QUALIFIED_NAME).getValue());
+
+ initializeContainer(container);
+ counter.reset();
+ container.addItemAt(container.size(), cn1);
+ counter.assertOnce();
+ Assert.assertEquals("com.example.Test",
+ container.getContainerProperty(container.lastItemId(),
+ FULLY_QUALIFIED_NAME).getValue());
+
+ initializeContainer(container);
+ counter.reset();
+ container.removeItem(nameToBean.get(sampleData[0]));
+ counter.assertOnce();
+
+ initializeContainer(container);
+ counter.reset();
+ // no notification for removing a non-existing item
+ container.removeItem(cn1);
+ counter.assertNone();
+
+ initializeContainer(container);
+ counter.reset();
+ container.removeAllItems();
+ counter.assertOnce();
+ // already empty
+ container.removeAllItems();
+ counter.assertNone();
+
+ }
+
+ @Test
+ public void testItemSetChangeListenersFiltering() {
+ BeanItemContainer<ClassName> container = getContainer();
+ ItemSetChangeCounter counter = new ItemSetChangeCounter();
+ container.addListener(counter);
+
+ ClassName cn1 = new ClassName("com.example.Test", 1111);
+ ClassName cn2 = new ClassName("com.example.Test2", 2222);
+ ClassName other = new ClassName("com.example.Other", 3333);
+
+ // simply adding or removing container filters should cause event
+ // (content changes)
+
+ initializeContainer(container);
+ counter.reset();
+ container.addContainerFilter(SIMPLE_NAME, "a", true, false);
+ counter.assertOnce();
+ container.removeContainerFilters(SIMPLE_NAME);
+ counter.assertOnce();
+
+ initializeContainer(container);
+ counter.reset();
+ container.addContainerFilter(SIMPLE_NAME, "a", true, false);
+ counter.assertOnce();
+ container.removeAllContainerFilters();
+ counter.assertOnce();
+
+ // perform operations while filtering container
+
+ initializeContainer(container);
+ counter.reset();
+ container.addContainerFilter(FULLY_QUALIFIED_NAME, "Test", true, false);
+ counter.assertOnce();
+
+ // passes filter
+ container.addBean(cn1);
+ counter.assertOnce();
+
+ // passes filter but already in the container
+ container.addBean(cn1);
+ counter.assertNone();
+
+ initializeContainer(container);
+ counter.reset();
+
+ // passes filter
+ container.addItem(cn1);
+ counter.assertOnce();
+ // already in the container
+ container.addItem(cn1);
+ counter.assertNone();
+ container.addItem(cn2);
+ counter.assertOnce();
+ // does not pass filter
+ container.addItem(other);
+ counter.assertNone();
+
+ initializeContainer(container);
+ counter.reset();
+ container.addItemAfter(null, cn1);
+ counter.assertOnce();
+ Assert.assertEquals("com.example.Test",
+ container.getContainerProperty(container.firstItemId(),
+ FULLY_QUALIFIED_NAME).getValue());
+
+ initializeContainer(container);
+ counter.reset();
+ container.addItemAfter(container.firstItemId(), cn1);
+ counter.assertOnce();
+ Assert.assertEquals("com.example.Test",
+ container.getContainerProperty(container.getIdByIndex(1),
+ FULLY_QUALIFIED_NAME).getValue());
+
+ initializeContainer(container);
+ counter.reset();
+ container.addItemAfter(container.lastItemId(), cn1);
+ counter.assertOnce();
+ Assert.assertEquals("com.example.Test",
+ container.getContainerProperty(container.lastItemId(),
+ FULLY_QUALIFIED_NAME).getValue());
+
+ initializeContainer(container);
+ counter.reset();
+ container.addItemAt(0, cn1);
+ counter.assertOnce();
+ Assert.assertEquals("com.example.Test",
+ container.getContainerProperty(container.firstItemId(),
+ FULLY_QUALIFIED_NAME).getValue());
+
+ initializeContainer(container);
+ counter.reset();
+ container.addItemAt(1, cn1);
+ counter.assertOnce();
+ Assert.assertEquals("com.example.Test",
+ container.getContainerProperty(container.getIdByIndex(1),
+ FULLY_QUALIFIED_NAME).getValue());
+
+ initializeContainer(container);
+ counter.reset();
+ container.addItemAt(container.size(), cn1);
+ counter.assertOnce();
+ Assert.assertEquals("com.example.Test",
+ container.getContainerProperty(container.lastItemId(),
+ FULLY_QUALIFIED_NAME).getValue());
+
+ // does not pass filter
+ // note: testAddRemoveWhileFiltering() checks position for these after
+ // removing filter etc, here concentrating on listeners
+
+ initializeContainer(container);
+ counter.reset();
+ container.addItemAfter(null, other);
+ counter.assertNone();
+
+ initializeContainer(container);
+ counter.reset();
+ container.addItemAfter(container.firstItemId(), other);
+ counter.assertNone();
+
+ initializeContainer(container);
+ counter.reset();
+ container.addItemAfter(container.lastItemId(), other);
+ counter.assertNone();
+
+ initializeContainer(container);
+ counter.reset();
+ container.addItemAt(0, other);
+ counter.assertNone();
+
+ initializeContainer(container);
+ counter.reset();
+ container.addItemAt(1, other);
+ counter.assertNone();
+
+ initializeContainer(container);
+ counter.reset();
+ container.addItemAt(container.size(), other);
+ counter.assertNone();
+
+ // passes filter
+
+ initializeContainer(container);
+ counter.reset();
+ container.addItem(cn1);
+ counter.assertOnce();
+ container.removeItem(cn1);
+ counter.assertOnce();
+
+ // does not pass filter
+
+ initializeContainer(container);
+ counter.reset();
+ // not visible
+ container.removeItem(nameToBean.get(sampleData[0]));
+ counter.assertNone();
+
+ container.removeAllItems();
+ counter.assertOnce();
+ // no visible items
+ container.removeAllItems();
+ counter.assertNone();
+ }
+
+ @Test
+ public void testAddRemoveWhileFiltering() {
+ BeanItemContainer<Person> container = new BeanItemContainer<Person>(
+ Person.class);
+
+ Person john = new Person("John");
+ Person jane = new Person("Jane");
+ Person matthew = new Person("Matthew");
+
+ Person jack = new Person("Jack");
+ Person michael = new Person("Michael");
+ Person william = new Person("William");
+ Person julia = new Person("Julia");
+ Person george = new Person("George");
+ Person mark = new Person("Mark");
+
+ container.addBean(john);
+ container.addBean(jane);
+ container.addBean(matthew);
+
+ assertEquals(3, container.size());
+ // john, jane, matthew
+
+ container.addContainerFilter("name", "j", true, true);
+
+ assertEquals(2, container.size());
+ // john, jane, (matthew)
+
+ // add a bean that passes the filter
+ container.addBean(jack);
+ assertEquals(3, container.size());
+ assertEquals(jack, container.lastItemId());
+ // john, jane, (matthew), jack
+
+ // add beans that do not pass the filter
+ container.addBean(michael);
+ // john, jane, (matthew), jack, (michael)
+ container.addItemAfter(null, william);
+ // (william), john, jane, (matthew), jack, (michael)
+
+ // add after an item that is shown
+ container.addItemAfter(john, george);
+ // (william), john, (george), jane, (matthew), jack, (michael)
+ assertEquals(3, container.size());
+ assertEquals(john, container.firstItemId());
+
+ // add after an item that is not shown does nothing
+ container.addItemAfter(william, julia);
+ // (william), john, (george), jane, (matthew), jack, (michael)
+ assertEquals(3, container.size());
+ assertEquals(john, container.firstItemId());
+
+ container.addItemAt(1, julia);
+ // (william), john, julia, (george), jane, (matthew), jack, (michael)
+
+ container.addItemAt(2, mark);
+ // (william), john, julia, (mark), (george), jane, (matthew), jack,
+ // (michael)
+
+ container.removeItem(matthew);
+ // (william), john, julia, (mark), (george), jane, jack, (michael)
+
+ assertEquals(4, container.size());
+ assertEquals(jack, container.lastItemId());
+
+ container.removeContainerFilters("name");
+
+ assertEquals(8, container.size());
+ assertEquals(william, container.firstItemId());
+ assertEquals(john, container.nextItemId(william));
+ assertEquals(julia, container.nextItemId(john));
+ assertEquals(mark, container.nextItemId(julia));
+ assertEquals(george, container.nextItemId(mark));
+ assertEquals(jane, container.nextItemId(george));
+ assertEquals(jack, container.nextItemId(jane));
+ assertEquals(michael, container.lastItemId());
+ }
+
+ @Test
+ public void testRefilterOnPropertyModification() {
+ BeanItemContainer<Person> container = new BeanItemContainer<Person>(
+ Person.class);
+
+ Person john = new Person("John");
+ Person jane = new Person("Jane");
+ Person matthew = new Person("Matthew");
+
+ container.addBean(john);
+ container.addBean(jane);
+ container.addBean(matthew);
+
+ assertEquals(3, container.size());
+ // john, jane, matthew
+
+ container.addContainerFilter("name", "j", true, true);
+
+ assertEquals(2, container.size());
+ // john, jane, (matthew)
+
+ // #6053 currently, modification of an item that is not visible does not
+ // trigger refiltering - should it?
+ // matthew.setName("Julia");
+ // assertEquals(3, container.size());
+ // john, jane, julia
+
+ john.setName("Mark");
+ assertEquals(2, container.size());
+ // (mark), jane, julia
+
+ container.removeAllContainerFilters();
+
+ assertEquals(3, container.size());
+ }
+
+ @Test
+ public void testAddAll() {
+ BeanItemContainer<Person> container = new BeanItemContainer<Person>(
+ Person.class);
+
+ Person john = new Person("John");
+ Person jane = new Person("Jane");
+ Person matthew = new Person("Matthew");
+
+ container.addBean(john);
+ container.addBean(jane);
+ container.addBean(matthew);
+
+ assertEquals(3, container.size());
+ // john, jane, matthew
+
+ Person jack = new Person("Jack");
+ Person michael = new Person("Michael");
+
+ // addAll
+ container.addAll(Arrays.asList(jack, michael));
+ // john, jane, matthew, jack, michael
+
+ assertEquals(5, container.size());
+ assertEquals(jane, container.nextItemId(john));
+ assertEquals(matthew, container.nextItemId(jane));
+ assertEquals(jack, container.nextItemId(matthew));
+ assertEquals(michael, container.nextItemId(jack));
+ }
+
+ @Test
+ public void testUnsupportedMethods() {
+ BeanItemContainer<Person> container = new BeanItemContainer<Person>(
+ Person.class);
+ container.addBean(new Person("John"));
+
+ try {
+ container.addItem();
+ Assert.fail();
+ } catch (UnsupportedOperationException e) {
+ // should get exception
+ }
+
+ try {
+ container.addItemAfter(new Person("Jane"));
+ Assert.fail();
+ } catch (UnsupportedOperationException e) {
+ // should get exception
+ }
+
+ try {
+ container.addItemAt(0);
+ Assert.fail();
+ } catch (UnsupportedOperationException e) {
+ // should get exception
+ }
+
+ try {
+ container.addContainerProperty("lastName", String.class, "");
+ Assert.fail();
+ } catch (UnsupportedOperationException e) {
+ // should get exception
+ }
+
+ assertEquals(1, container.size());
+ }
+
+ @Test
+ public void testRemoveContainerProperty() {
+ BeanItemContainer<Person> container = new BeanItemContainer<Person>(
+ Person.class);
+ Person john = new Person("John");
+ container.addBean(john);
+
+ Assert.assertEquals("John",
+ container.getContainerProperty(john, "name").getValue());
+ Assert.assertTrue(container.removeContainerProperty("name"));
+ Assert.assertNull(container.getContainerProperty(john, "name"));
+
+ Assert.assertNotNull(container.getItem(john));
+ // property removed also from item
+ Assert.assertNull(container.getItem(john).getItemProperty("name"));
+ }
+
+ @Test
+ public void testAddNullBean() {
+ BeanItemContainer<Person> container = new BeanItemContainer<Person>(
+ Person.class);
+ Person john = new Person("John");
+ container.addBean(john);
+
+ assertNull(container.addItem(null));
+ assertNull(container.addItemAfter(null, null));
+ assertNull(container.addItemAfter(john, null));
+ assertNull(container.addItemAt(0, null));
+
+ assertEquals(1, container.size());
+ }
+
+ @Test
+ public void testBeanIdResolver() {
+ BeanItemContainer<Person> container = new BeanItemContainer<Person>(
+ Person.class);
+ Person john = new Person("John");
+
+ assertSame(john, container.getBeanIdResolver().getIdForBean(john));
+ }
+
+ @Test(expected = IllegalArgumentException.class)
+ public void testNullBeanClass() {
+ new BeanItemContainer<Object>((Class<Object>) null);
+ }
+
+ @Test
+ public void testAddNestedContainerProperty() {
+ BeanItemContainer<NestedMethodPropertyTest.Person> container = new BeanItemContainer<NestedMethodPropertyTest.Person>(
+ NestedMethodPropertyTest.Person.class);
+
+ NestedMethodPropertyTest.Person john = new NestedMethodPropertyTest.Person(
+ "John",
+ new NestedMethodPropertyTest.Address("Ruukinkatu 2-4", 20540));
+ container.addBean(john);
+
+ assertTrue(container.addNestedContainerProperty("address.street"));
+ assertEquals("Ruukinkatu 2-4", container
+ .getContainerProperty(john, "address.street").getValue());
+ }
+
+ @Test
+ public void testNestedContainerPropertyWithNullBean() {
+ BeanItemContainer<NestedMethodPropertyTest.Person> container = new BeanItemContainer<NestedMethodPropertyTest.Person>(
+ NestedMethodPropertyTest.Person.class);
+ NestedMethodPropertyTest.Person john = new NestedMethodPropertyTest.Person(
+ "John", null);
+ assertNotNull(container.addBean(john));
+ assertTrue(container
+ .addNestedContainerProperty("address.postalCodeObject"));
+ assertTrue(container.addNestedContainerProperty("address.street"));
+ // the nested properties should return null
+ assertNull(container.getContainerProperty(john, "address.street")
+ .getValue());
+ }
+
+ @Test
+ public void testItemAddedEvent() {
+ BeanItemContainer<Person> container = new BeanItemContainer<Person>(
+ Person.class);
+ Person bean = new Person("John");
+ ItemSetChangeListener addListener = createListenerMockFor(container);
+ addListener.containerItemSetChange(EasyMock.isA(ItemAddEvent.class));
+ EasyMock.replay(addListener);
+
+ container.addItem(bean);
+
+ EasyMock.verify(addListener);
+ }
+
+ @Test
+ public void testItemAddedEvent_AddedItem() {
+ BeanItemContainer<Person> container = new BeanItemContainer<Person>(
+ Person.class);
+ Person bean = new Person("John");
+ ItemSetChangeListener addListener = createListenerMockFor(container);
+ Capture<ItemAddEvent> capturedEvent = captureAddEvent(addListener);
+ EasyMock.replay(addListener);
+
+ container.addItem(bean);
+
+ assertEquals(bean, capturedEvent.getValue().getFirstItemId());
+ }
+
+ @Test
+ public void testItemAddedEvent_addItemAt_IndexOfAddedItem() {
+ BeanItemContainer<Person> container = new BeanItemContainer<Person>(
+ Person.class);
+ Person bean = new Person("John");
+ container.addItem(bean);
+ ItemSetChangeListener addListener = createListenerMockFor(container);
+ Capture<ItemAddEvent> capturedEvent = captureAddEvent(addListener);
+ EasyMock.replay(addListener);
+
+ container.addItemAt(1, new Person(""));
+
+ assertEquals(1, capturedEvent.getValue().getFirstIndex());
+ }
+
+ @Test
+ public void testItemAddedEvent_addItemAfter_IndexOfAddedItem() {
+ BeanItemContainer<Person> container = new BeanItemContainer<Person>(
+ Person.class);
+ Person bean = new Person("John");
+ container.addItem(bean);
+ ItemSetChangeListener addListener = createListenerMockFor(container);
+ Capture<ItemAddEvent> capturedEvent = captureAddEvent(addListener);
+ EasyMock.replay(addListener);
+
+ container.addItemAfter(bean, new Person(""));
+
+ assertEquals(1, capturedEvent.getValue().getFirstIndex());
+ }
+
+ @Test
+ public void testItemAddedEvent_amountOfAddedItems() {
+ BeanItemContainer<Person> container = new BeanItemContainer<Person>(
+ Person.class);
+ ItemSetChangeListener addListener = createListenerMockFor(container);
+ Capture<ItemAddEvent> capturedEvent = captureAddEvent(addListener);
+ EasyMock.replay(addListener);
+ List<Person> beans = Arrays.asList(new Person("Jack"),
+ new Person("John"));
+
+ container.addAll(beans);
+
+ assertEquals(2, capturedEvent.getValue().getAddedItemsCount());
+ }
+
+ @Test
+ public void testItemAddedEvent_someItemsAreFiltered_amountOfAddedItemsIsReducedByAmountOfFilteredItems() {
+ BeanItemContainer<Person> container = new BeanItemContainer<Person>(
+ Person.class);
+ ItemSetChangeListener addListener = createListenerMockFor(container);
+ Capture<ItemAddEvent> capturedEvent = captureAddEvent(addListener);
+ EasyMock.replay(addListener);
+ List<Person> beans = Arrays.asList(new Person("Jack"),
+ new Person("John"));
+ container.addFilter(new Compare.Equal("name", "John"));
+
+ container.addAll(beans);
+
+ assertEquals(1, capturedEvent.getValue().getAddedItemsCount());
+ }
+
+ @Test
+ public void testItemAddedEvent_someItemsAreFiltered_addedItemIsTheFirstVisibleItem() {
+ BeanItemContainer<Person> container = new BeanItemContainer<Person>(
+ Person.class);
+ Person bean = new Person("John");
+ ItemSetChangeListener addListener = createListenerMockFor(container);
+ Capture<ItemAddEvent> capturedEvent = captureAddEvent(addListener);
+ EasyMock.replay(addListener);
+ List<Person> beans = Arrays.asList(new Person("Jack"), bean);
+ container.addFilter(new Compare.Equal("name", "John"));
+
+ container.addAll(beans);
+
+ assertEquals(bean, capturedEvent.getValue().getFirstItemId());
+ }
+
+ @Test
+ public void testItemRemovedEvent() {
+ BeanItemContainer<Person> container = new BeanItemContainer<Person>(
+ Person.class);
+ Person bean = new Person("John");
+ container.addItem(bean);
+ ItemSetChangeListener removeListener = createListenerMockFor(container);
+ removeListener
+ .containerItemSetChange(EasyMock.isA(ItemRemoveEvent.class));
+ EasyMock.replay(removeListener);
+
+ container.removeItem(bean);
+
+ EasyMock.verify(removeListener);
+ }
+
+ @Test
+ public void testItemRemovedEvent_RemovedItem() {
+ BeanItemContainer<Person> container = new BeanItemContainer<Person>(
+ Person.class);
+ Person bean = new Person("John");
+ container.addItem(bean);
+ ItemSetChangeListener removeListener = createListenerMockFor(container);
+ Capture<ItemRemoveEvent> capturedEvent = captureRemoveEvent(
+ removeListener);
+ EasyMock.replay(removeListener);
+
+ container.removeItem(bean);
+
+ assertEquals(bean, capturedEvent.getValue().getFirstItemId());
+ }
+
+ @Test
+ public void testItemRemovedEvent_indexOfRemovedItem() {
+ BeanItemContainer<Person> container = new BeanItemContainer<Person>(
+ Person.class);
+ container.addItem(new Person("Jack"));
+ Person secondBean = new Person("John");
+ container.addItem(secondBean);
+ ItemSetChangeListener removeListener = createListenerMockFor(container);
+ Capture<ItemRemoveEvent> capturedEvent = captureRemoveEvent(
+ removeListener);
+ EasyMock.replay(removeListener);
+
+ container.removeItem(secondBean);
+
+ assertEquals(1, capturedEvent.getValue().getFirstIndex());
+ }
+
+ @Test
+ public void testItemRemovedEvent_amountOfRemovedItems() {
+ BeanItemContainer<Person> container = new BeanItemContainer<Person>(
+ Person.class);
+ container.addItem(new Person("Jack"));
+ container.addItem(new Person("John"));
+ ItemSetChangeListener removeListener = createListenerMockFor(container);
+ Capture<ItemRemoveEvent> capturedEvent = captureRemoveEvent(
+ removeListener);
+ EasyMock.replay(removeListener);
+
+ container.removeAllItems();
+
+ assertEquals(2, capturedEvent.getValue().getRemovedItemsCount());
+ }
+
+ private Capture<ItemAddEvent> captureAddEvent(
+ ItemSetChangeListener addListener) {
+ Capture<ItemAddEvent> capturedEvent = new Capture<ItemAddEvent>();
+ addListener.containerItemSetChange(EasyMock.capture(capturedEvent));
+ return capturedEvent;
+ }
+
+ private Capture<ItemRemoveEvent> captureRemoveEvent(
+ ItemSetChangeListener removeListener) {
+ Capture<ItemRemoveEvent> capturedEvent = new Capture<ItemRemoveEvent>();
+ removeListener.containerItemSetChange(EasyMock.capture(capturedEvent));
+ return capturedEvent;
+ }
+
+ private ItemSetChangeListener createListenerMockFor(
+ BeanItemContainer<Person> container) {
+ ItemSetChangeListener listener = EasyMock
+ .createNiceMock(ItemSetChangeListener.class);
+ container.addItemSetChangeListener(listener);
+ return listener;
+ }
+
+ @Test
+ public void testAddNestedContainerBeanBeforeData() {
+ BeanItemContainer<NestedMethodPropertyTest.Person> container = new BeanItemContainer<NestedMethodPropertyTest.Person>(
+ NestedMethodPropertyTest.Person.class);
+
+ container.addNestedContainerBean("address");
+
+ assertTrue(
+ container.getContainerPropertyIds().contains("address.street"));
+
+ NestedMethodPropertyTest.Person john = new NestedMethodPropertyTest.Person(
+ "John", new Address("streetname", 12345));
+ container.addBean(john);
+
+ assertTrue(container.getItem(john).getItemPropertyIds()
+ .contains("address.street"));
+ assertEquals("streetname", container.getItem(john)
+ .getItemProperty("address.street").getValue());
+
+ }
+
+ @Test
+ public void testAddNestedContainerBeanAfterData() {
+ BeanItemContainer<NestedMethodPropertyTest.Person> container = new BeanItemContainer<NestedMethodPropertyTest.Person>(
+ NestedMethodPropertyTest.Person.class);
+
+ NestedMethodPropertyTest.Person john = new NestedMethodPropertyTest.Person(
+ "John", new Address("streetname", 12345));
+ container.addBean(john);
+
+ container.addNestedContainerBean("address");
+
+ assertTrue(
+ container.getContainerPropertyIds().contains("address.street"));
+ assertTrue(container.getItem(john).getItemPropertyIds()
+ .contains("address.street"));
+ assertEquals("streetname", container.getItem(john)
+ .getItemProperty("address.street").getValue());
+
+ }
+}
diff --git a/compatibility-server/src/test/java/com/vaadin/data/util/BeanItemTest.java b/compatibility-server/src/test/java/com/vaadin/data/util/BeanItemTest.java
new file mode 100644
index 0000000000..1bbe74818c
--- /dev/null
+++ b/compatibility-server/src/test/java/com/vaadin/data/util/BeanItemTest.java
@@ -0,0 +1,389 @@
+package com.vaadin.data.util;
+
+import java.lang.reflect.Constructor;
+import java.lang.reflect.InvocationTargetException;
+import java.lang.reflect.Method;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Iterator;
+import java.util.LinkedHashMap;
+import java.util.Map;
+
+import org.junit.Assert;
+import org.junit.Test;
+
+import com.vaadin.data.Property;
+
+/**
+ * Test BeanItem specific features.
+ *
+ * Only public API is tested, not the methods with package visibility.
+ *
+ * See also {@link PropertySetItemTest}, which tests the base class.
+ */
+public class BeanItemTest {
+
+ @SuppressWarnings("unused")
+ protected static class MySuperClass {
+ private int superPrivate = 1;
+ private int superPrivate2 = 2;
+ protected double superProtected = 3.0;
+ private double superProtected2 = 4.0;
+ public boolean superPublic = true;
+ private boolean superPublic2 = true;
+
+ public int getSuperPrivate() {
+ return superPrivate;
+ }
+
+ public void setSuperPrivate(int superPrivate) {
+ this.superPrivate = superPrivate;
+ }
+
+ public double getSuperProtected() {
+ return superProtected;
+ }
+
+ public void setSuperProtected(double superProtected) {
+ this.superProtected = superProtected;
+ }
+
+ public boolean isSuperPublic() {
+ return superPublic;
+ }
+
+ public void setSuperPublic(boolean superPublic) {
+ this.superPublic = superPublic;
+ }
+
+ }
+
+ protected static class MyClass extends MySuperClass {
+ private String name;
+ public int value = 123;
+
+ public MyClass(String name) {
+ this.name = name;
+ }
+
+ public void setName(String name) {
+ this.name = name;
+ }
+
+ public String getName() {
+ return name;
+ }
+
+ public void setNoField(String name) {
+ }
+
+ public String getNoField() {
+ return "no field backing this setter";
+ }
+
+ public String getName2() {
+ return name;
+ }
+ }
+
+ protected static class MyClass2 extends MyClass {
+ public MyClass2(String name) {
+ super(name);
+ }
+
+ @Override
+ public void setName(String name) {
+ super.setName(name + "2");
+ }
+
+ @Override
+ public String getName() {
+ return super.getName() + "2";
+ }
+
+ @Override
+ public String getName2() {
+ return super.getName();
+ }
+
+ public void setName2(String name) {
+ super.setName(name);
+ }
+ }
+
+ protected static interface MySuperInterface {
+ public int getSuper1();
+
+ public void setSuper1(int i);
+
+ public int getOverride();
+ }
+
+ protected static interface MySuperInterface2 {
+ public int getSuper2();
+ }
+
+ protected static class Generic<T> {
+
+ public T getProperty() {
+ return null;
+ }
+
+ public void setProperty(T t) {
+ throw new UnsupportedOperationException();
+ }
+ }
+
+ protected static class SubClass extends Generic<String> {
+
+ @Override
+ // Has a bridged method
+ public String getProperty() {
+ return "";
+ }
+
+ @Override
+ // Has a bridged method
+ public void setProperty(String t) {
+ }
+ }
+
+ protected static interface MySubInterface
+ extends MySuperInterface, MySuperInterface2 {
+ public int getSub();
+
+ public void setSub(int i);
+
+ @Override
+ public int getOverride();
+
+ public void setOverride(int i);
+ }
+
+ @Test
+ public void testGetProperties() {
+ BeanItem<MySuperClass> item = new BeanItem<MySuperClass>(
+ new MySuperClass());
+
+ Collection<?> itemPropertyIds = item.getItemPropertyIds();
+ Assert.assertEquals(3, itemPropertyIds.size());
+ Assert.assertTrue(itemPropertyIds.contains("superPrivate"));
+ Assert.assertTrue(itemPropertyIds.contains("superProtected"));
+ Assert.assertTrue(itemPropertyIds.contains("superPublic"));
+ }
+
+ @Test
+ public void testGetSuperClassProperties() {
+ BeanItem<MyClass> item = new BeanItem<MyClass>(new MyClass("bean1"));
+
+ Collection<?> itemPropertyIds = item.getItemPropertyIds();
+ Assert.assertEquals(6, itemPropertyIds.size());
+ Assert.assertTrue(itemPropertyIds.contains("superPrivate"));
+ Assert.assertTrue(itemPropertyIds.contains("superProtected"));
+ Assert.assertTrue(itemPropertyIds.contains("superPublic"));
+ Assert.assertTrue(itemPropertyIds.contains("name"));
+ Assert.assertTrue(itemPropertyIds.contains("noField"));
+ Assert.assertTrue(itemPropertyIds.contains("name2"));
+ }
+
+ @Test
+ public void testOverridingProperties() {
+ BeanItem<MyClass2> item = new BeanItem<MyClass2>(new MyClass2("bean2"));
+
+ Collection<?> itemPropertyIds = item.getItemPropertyIds();
+ Assert.assertEquals(6, itemPropertyIds.size());
+
+ Assert.assertTrue(MyClass2.class.equals(item.getBean().getClass()));
+
+ // check that name2 accessed via MyClass2, not MyClass
+ Assert.assertFalse(item.getItemProperty("name2").isReadOnly());
+ }
+
+ @Test
+ public void testGetInterfaceProperties() throws SecurityException,
+ NoSuchMethodException, IllegalArgumentException,
+ IllegalAccessException, InvocationTargetException {
+ Method method = BeanItem.class
+ .getDeclaredMethod("getPropertyDescriptors", Class.class);
+ method.setAccessible(true);
+ LinkedHashMap<String, VaadinPropertyDescriptor<Class>> propertyDescriptors = (LinkedHashMap<String, VaadinPropertyDescriptor<Class>>) method
+ .invoke(null, MySuperInterface.class);
+
+ Assert.assertEquals(2, propertyDescriptors.size());
+ Assert.assertTrue(propertyDescriptors.containsKey("super1"));
+ Assert.assertTrue(propertyDescriptors.containsKey("override"));
+
+ MethodProperty<?> property = (MethodProperty<?>) propertyDescriptors
+ .get("override").createProperty(getClass());
+ Assert.assertTrue(property.isReadOnly());
+ }
+
+ @Test
+ public void testGetSuperInterfaceProperties() throws SecurityException,
+ NoSuchMethodException, IllegalArgumentException,
+ IllegalAccessException, InvocationTargetException {
+ Method method = BeanItem.class
+ .getDeclaredMethod("getPropertyDescriptors", Class.class);
+ method.setAccessible(true);
+ LinkedHashMap<String, VaadinPropertyDescriptor<Class>> propertyDescriptors = (LinkedHashMap<String, VaadinPropertyDescriptor<Class>>) method
+ .invoke(null, MySubInterface.class);
+
+ Assert.assertEquals(4, propertyDescriptors.size());
+ Assert.assertTrue(propertyDescriptors.containsKey("sub"));
+ Assert.assertTrue(propertyDescriptors.containsKey("super1"));
+ Assert.assertTrue(propertyDescriptors.containsKey("super2"));
+ Assert.assertTrue(propertyDescriptors.containsKey("override"));
+
+ MethodProperty<?> property = (MethodProperty<?>) propertyDescriptors
+ .get("override").createProperty(getClass());
+ Assert.assertFalse(property.isReadOnly());
+ }
+
+ @Test
+ public void testPropertyExplicitOrder() {
+ Collection<String> ids = new ArrayList<String>();
+ ids.add("name");
+ ids.add("superPublic");
+ ids.add("name2");
+ ids.add("noField");
+
+ BeanItem<MyClass> item = new BeanItem<MyClass>(new MyClass("bean1"),
+ ids);
+
+ Iterator<?> it = item.getItemPropertyIds().iterator();
+ Assert.assertEquals("name", it.next());
+ Assert.assertEquals("superPublic", it.next());
+ Assert.assertEquals("name2", it.next());
+ Assert.assertEquals("noField", it.next());
+ Assert.assertFalse(it.hasNext());
+ }
+
+ @Test
+ public void testPropertyExplicitOrder2() {
+ BeanItem<MyClass> item = new BeanItem<MyClass>(new MyClass("bean1"),
+ new String[] { "name", "superPublic", "name2", "noField" });
+
+ Iterator<?> it = item.getItemPropertyIds().iterator();
+ Assert.assertEquals("name", it.next());
+ Assert.assertEquals("superPublic", it.next());
+ Assert.assertEquals("name2", it.next());
+ Assert.assertEquals("noField", it.next());
+ Assert.assertFalse(it.hasNext());
+ }
+
+ @Test
+ public void testPropertyBadPropertyName() {
+ Collection<String> ids = new ArrayList<String>();
+ ids.add("name3");
+ ids.add("name");
+
+ // currently silently ignores non-existent properties
+ BeanItem<MyClass> item = new BeanItem<MyClass>(new MyClass("bean1"),
+ ids);
+
+ Iterator<?> it = item.getItemPropertyIds().iterator();
+ Assert.assertEquals("name", it.next());
+ Assert.assertFalse(it.hasNext());
+ }
+
+ @Test
+ public void testRemoveProperty() {
+ BeanItem<MyClass> item = new BeanItem<MyClass>(new MyClass("bean1"));
+
+ Collection<?> itemPropertyIds = item.getItemPropertyIds();
+ Assert.assertEquals(6, itemPropertyIds.size());
+
+ item.removeItemProperty("name2");
+ Assert.assertEquals(5, itemPropertyIds.size());
+ Assert.assertFalse(itemPropertyIds.contains("name2"));
+ }
+
+ @Test
+ public void testRemoveSuperProperty() {
+ BeanItem<MyClass> item = new BeanItem<MyClass>(new MyClass("bean1"));
+
+ Collection<?> itemPropertyIds = item.getItemPropertyIds();
+ Assert.assertEquals(6, itemPropertyIds.size());
+
+ item.removeItemProperty("superPrivate");
+ Assert.assertEquals(5, itemPropertyIds.size());
+ Assert.assertFalse(itemPropertyIds.contains("superPrivate"));
+ }
+
+ @Test
+ public void testPropertyTypes() {
+ BeanItem<MyClass> item = new BeanItem<MyClass>(new MyClass("bean1"));
+
+ Assert.assertTrue(Integer.class
+ .equals(item.getItemProperty("superPrivate").getType()));
+ Assert.assertTrue(Double.class
+ .equals(item.getItemProperty("superProtected").getType()));
+ Assert.assertTrue(Boolean.class
+ .equals(item.getItemProperty("superPublic").getType()));
+ Assert.assertTrue(
+ String.class.equals(item.getItemProperty("name").getType()));
+ }
+
+ @Test
+ public void testPropertyReadOnly() {
+ BeanItem<MyClass> item = new BeanItem<MyClass>(new MyClass("bean1"));
+
+ Assert.assertFalse(item.getItemProperty("name").isReadOnly());
+ Assert.assertTrue(item.getItemProperty("name2").isReadOnly());
+ }
+
+ @Test
+ public void testCustomProperties() throws Exception {
+ LinkedHashMap<String, VaadinPropertyDescriptor<MyClass>> propertyDescriptors = new LinkedHashMap<String, VaadinPropertyDescriptor<MyClass>>();
+ propertyDescriptors.put("myname",
+ new MethodPropertyDescriptor<BeanItemTest.MyClass>("myname",
+ MyClass.class,
+ MyClass.class.getDeclaredMethod("getName"),
+ MyClass.class.getDeclaredMethod("setName",
+ String.class)));
+ MyClass instance = new MyClass("bean1");
+ Constructor<BeanItem> constructor = BeanItem.class
+ .getDeclaredConstructor(Object.class, Map.class);
+ constructor.setAccessible(true);
+ BeanItem<MyClass> item = constructor.newInstance(instance,
+ propertyDescriptors);
+
+ Assert.assertEquals(1, item.getItemPropertyIds().size());
+ Assert.assertEquals("bean1", item.getItemProperty("myname").getValue());
+ }
+
+ @Test
+ public void testAddRemoveProperty() throws Exception {
+ MethodPropertyDescriptor<BeanItemTest.MyClass> pd = new MethodPropertyDescriptor<BeanItemTest.MyClass>(
+ "myname", MyClass.class,
+ MyClass.class.getDeclaredMethod("getName"),
+ MyClass.class.getDeclaredMethod("setName", String.class));
+
+ BeanItem<MyClass> item = new BeanItem<MyClass>(new MyClass("bean1"));
+
+ Assert.assertEquals(6, item.getItemPropertyIds().size());
+ Assert.assertEquals(null, item.getItemProperty("myname"));
+
+ item.addItemProperty("myname", pd.createProperty(item.getBean()));
+ Assert.assertEquals(7, item.getItemPropertyIds().size());
+ Assert.assertEquals("bean1", item.getItemProperty("myname").getValue());
+ item.removeItemProperty("myname");
+ Assert.assertEquals(6, item.getItemPropertyIds().size());
+ Assert.assertEquals(null, item.getItemProperty("myname"));
+ }
+
+ @Test
+ public void testOverridenGenericMethods() {
+ BeanItem<SubClass> item = new BeanItem<SubClass>(new SubClass());
+
+ Property<?> property = item.getItemProperty("property");
+ Assert.assertEquals("Unexpected class for property type", String.class,
+ property.getType());
+
+ Assert.assertEquals("Unexpected property value", "",
+ property.getValue());
+
+ // Should not be exception
+ property.setValue(null);
+ }
+}
diff --git a/compatibility-server/src/test/java/com/vaadin/data/util/ContainerHierarchicalWrapperTest.java b/compatibility-server/src/test/java/com/vaadin/data/util/ContainerHierarchicalWrapperTest.java
new file mode 100644
index 0000000000..e41f751bfd
--- /dev/null
+++ b/compatibility-server/src/test/java/com/vaadin/data/util/ContainerHierarchicalWrapperTest.java
@@ -0,0 +1,26 @@
+package com.vaadin.data.util;
+
+import org.junit.Test;
+
+public class ContainerHierarchicalWrapperTest
+ extends AbstractHierarchicalContainerTestBase {
+
+ @Test
+ public void testBasicOperations() {
+ testBasicContainerOperations(
+ new ContainerHierarchicalWrapper(new IndexedContainer()));
+ }
+
+ @Test
+ public void testHierarchicalContainer() {
+ testHierarchicalContainer(
+ new ContainerHierarchicalWrapper(new IndexedContainer()));
+ }
+
+ @Test
+ public void testRemoveSubtree() {
+ testRemoveHierarchicalWrapperSubtree(
+ new ContainerHierarchicalWrapper(new IndexedContainer()));
+ }
+
+}
diff --git a/compatibility-server/src/test/java/com/vaadin/data/util/ContainerOrderedWrapperTest.java b/compatibility-server/src/test/java/com/vaadin/data/util/ContainerOrderedWrapperTest.java
new file mode 100644
index 0000000000..c3a8b2c4f9
--- /dev/null
+++ b/compatibility-server/src/test/java/com/vaadin/data/util/ContainerOrderedWrapperTest.java
@@ -0,0 +1,107 @@
+package com.vaadin.data.util;
+
+import java.util.Collection;
+
+import org.junit.Test;
+
+import com.vaadin.data.Container;
+import com.vaadin.data.Item;
+import com.vaadin.data.Property;
+
+public class ContainerOrderedWrapperTest extends AbstractContainerTestBase {
+
+ // This class is needed to get an implementation of container
+ // which is not an implementation of Ordered interface.
+ private class NotOrderedContainer implements Container {
+
+ private IndexedContainer container;
+
+ public NotOrderedContainer() {
+ container = new IndexedContainer();
+ }
+
+ @Override
+ public Item getItem(Object itemId) {
+ return container.getItem(itemId);
+ }
+
+ @Override
+ public Collection<?> getContainerPropertyIds() {
+ return container.getContainerPropertyIds();
+ }
+
+ @Override
+ public Collection<?> getItemIds() {
+ return container.getItemIds();
+ }
+
+ @Override
+ public Property getContainerProperty(Object itemId, Object propertyId) {
+ return container.getContainerProperty(itemId, propertyId);
+ }
+
+ @Override
+ public Class<?> getType(Object propertyId) {
+ return container.getType(propertyId);
+ }
+
+ @Override
+ public int size() {
+ return container.size();
+ }
+
+ @Override
+ public boolean containsId(Object itemId) {
+ return container.containsId(itemId);
+ }
+
+ @Override
+ public Item addItem(Object itemId)
+ throws UnsupportedOperationException {
+ return container.addItem(itemId);
+ }
+
+ @Override
+ public Object addItem() throws UnsupportedOperationException {
+ return container.addItem();
+ }
+
+ @Override
+ public boolean removeItem(Object itemId)
+ throws UnsupportedOperationException {
+ return container.removeItem(itemId);
+ }
+
+ @Override
+ public boolean addContainerProperty(Object propertyId, Class<?> type,
+ Object defaultValue) throws UnsupportedOperationException {
+ return container.addContainerProperty(propertyId, type,
+ defaultValue);
+ }
+
+ @Override
+ public boolean removeContainerProperty(Object propertyId)
+ throws UnsupportedOperationException {
+ return container.removeContainerProperty(propertyId);
+ }
+
+ @Override
+ public boolean removeAllItems() throws UnsupportedOperationException {
+ return container.removeAllItems();
+ }
+
+ }
+
+ @Test
+ public void testBasicOperations() {
+ testBasicContainerOperations(
+ new ContainerOrderedWrapper(new NotOrderedContainer()));
+ }
+
+ @Test
+ public void testOrdered() {
+ testContainerOrdered(
+ new ContainerOrderedWrapper(new NotOrderedContainer()));
+ }
+
+}
diff --git a/compatibility-server/src/test/java/com/vaadin/data/util/ContainerSizeAssertTest.java b/compatibility-server/src/test/java/com/vaadin/data/util/ContainerSizeAssertTest.java
new file mode 100644
index 0000000000..58cb2b1d27
--- /dev/null
+++ b/compatibility-server/src/test/java/com/vaadin/data/util/ContainerSizeAssertTest.java
@@ -0,0 +1,59 @@
+/*
+ * Copyright 2000-2016 Vaadin Ltd.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not
+ * use this file except in compliance with the License. You may obtain a copy of
+ * the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+package com.vaadin.data.util;
+
+import org.junit.Test;
+
+import com.vaadin.data.Container;
+import com.vaadin.ui.Table;
+
+public class ContainerSizeAssertTest {
+
+ @Test(expected = AssertionError.class)
+ public void testNegativeSizeAssert() {
+ Table table = createAttachedTable();
+
+ table.setContainerDataSource(createNegativeSizeContainer());
+ }
+
+ @Test
+ public void testZeroSizeNoAssert() {
+ Table table = createAttachedTable();
+
+ table.setContainerDataSource(new IndexedContainer());
+ }
+
+ private Container createNegativeSizeContainer() {
+ return new IndexedContainer() {
+ @Override
+ public int size() {
+ return -1;
+ }
+ };
+ }
+
+ private Table createAttachedTable() {
+ return new Table() {
+ private boolean initialized = true;
+
+ @Override
+ public boolean isAttached() {
+ // This returns false until the super constructor has finished
+ return initialized;
+ }
+ };
+ }
+}
diff --git a/compatibility-server/src/test/java/com/vaadin/data/util/ContainerSortingTest.java b/compatibility-server/src/test/java/com/vaadin/data/util/ContainerSortingTest.java
new file mode 100644
index 0000000000..1c4dc1de5e
--- /dev/null
+++ b/compatibility-server/src/test/java/com/vaadin/data/util/ContainerSortingTest.java
@@ -0,0 +1,222 @@
+package com.vaadin.data.util;
+
+import java.util.HashMap;
+import java.util.Iterator;
+import java.util.Map;
+
+import org.junit.Test;
+
+import com.vaadin.data.Container;
+import com.vaadin.data.Item;
+import com.vaadin.tests.util.TestUtil;
+
+public class ContainerSortingTest {
+
+ private static final String ITEM_DATA_MINUS2_NULL = "Data -2 null";
+ private static final String ITEM_DATA_MINUS2 = "Data -2";
+ private static final String ITEM_DATA_MINUS1 = "Data -1";
+ private static final String ITEM_DATA_MINUS1_NULL = "Data -1 null";
+ private static final String ITEM_ANOTHER_NULL = "Another null";
+ private static final String ITEM_STRING_2 = "String 2";
+ private static final String ITEM_STRING_NULL2 = "String null";
+ private static final String ITEM_STRING_1 = "String 1";
+
+ private static final String PROPERTY_INTEGER_NULL2 = "integer-null";
+ private static final String PROPERTY_INTEGER_NOT_NULL = "integer-not-null";
+ private static final String PROPERTY_STRING_NULL = "string-null";
+ private static final String PROPERTY_STRING_ID = "string-not-null";
+
+ @Test
+ public void testEmptyFilteredIndexedContainer() {
+ IndexedContainer ic = new IndexedContainer();
+
+ addProperties(ic);
+ populate(ic);
+
+ ic.addContainerFilter(PROPERTY_STRING_ID, "aasdfasdfasdf", true, false);
+ ic.sort(new Object[] { PROPERTY_STRING_ID }, new boolean[] { true });
+
+ }
+
+ @Test
+ public void testFilteredIndexedContainer() {
+ IndexedContainer ic = new IndexedContainer();
+
+ addProperties(ic);
+ populate(ic);
+
+ ic.addContainerFilter(PROPERTY_STRING_ID, "a", true, false);
+ ic.sort(new Object[] { PROPERTY_STRING_ID }, new boolean[] { true });
+ verifyOrder(ic,
+ new String[] { ITEM_ANOTHER_NULL, ITEM_DATA_MINUS1,
+ ITEM_DATA_MINUS1_NULL, ITEM_DATA_MINUS2,
+ ITEM_DATA_MINUS2_NULL, });
+ }
+
+ @Test
+ public void testIndexedContainer() {
+ IndexedContainer ic = new IndexedContainer();
+
+ addProperties(ic);
+ populate(ic);
+
+ ic.sort(new Object[] { PROPERTY_STRING_ID }, new boolean[] { true });
+ verifyOrder(ic, new String[] { ITEM_ANOTHER_NULL, ITEM_DATA_MINUS1,
+ ITEM_DATA_MINUS1_NULL, ITEM_DATA_MINUS2, ITEM_DATA_MINUS2_NULL,
+ ITEM_STRING_1, ITEM_STRING_2, ITEM_STRING_NULL2 });
+
+ ic.sort(new Object[] { PROPERTY_INTEGER_NOT_NULL,
+ PROPERTY_INTEGER_NULL2, PROPERTY_STRING_ID },
+ new boolean[] { true, false, true });
+ verifyOrder(ic, new String[] { ITEM_DATA_MINUS2, ITEM_DATA_MINUS2_NULL,
+ ITEM_DATA_MINUS1, ITEM_DATA_MINUS1_NULL, ITEM_ANOTHER_NULL,
+ ITEM_STRING_NULL2, ITEM_STRING_1, ITEM_STRING_2 });
+
+ ic.sort(new Object[] { PROPERTY_INTEGER_NOT_NULL,
+ PROPERTY_INTEGER_NULL2, PROPERTY_STRING_ID },
+ new boolean[] { true, true, true });
+ verifyOrder(ic, new String[] { ITEM_DATA_MINUS2_NULL, ITEM_DATA_MINUS2,
+ ITEM_DATA_MINUS1_NULL, ITEM_DATA_MINUS1, ITEM_ANOTHER_NULL,
+ ITEM_STRING_NULL2, ITEM_STRING_1, ITEM_STRING_2 });
+
+ }
+
+ @Test
+ public void testHierarchicalContainer() {
+ HierarchicalContainer hc = new HierarchicalContainer();
+ populateContainer(hc);
+ hc.sort(new Object[] { "name" }, new boolean[] { true });
+ verifyOrder(hc,
+ new String[] { "Audi", "C++", "Call of Duty", "Cars", "English",
+ "Fallout", "Finnish", "Ford", "Games", "Java",
+ "Might and Magic", "Natural languages", "PHP",
+ "Programming languages", "Python", "Red Alert",
+ "Swedish", "Toyota", "Volvo" });
+ TestUtil.assertArrays(hc.rootItemIds().toArray(),
+ new Integer[] { nameToId.get("Cars"), nameToId.get("Games"),
+ nameToId.get("Natural languages"),
+ nameToId.get("Programming languages") });
+ TestUtil.assertArrays(hc.getChildren(nameToId.get("Games")).toArray(),
+ new Integer[] { nameToId.get("Call of Duty"),
+ nameToId.get("Fallout"),
+ nameToId.get("Might and Magic"),
+ nameToId.get("Red Alert") });
+ }
+
+ private static void populateContainer(HierarchicalContainer container) {
+ container.addContainerProperty("name", String.class, null);
+
+ addItem(container, "Games", null);
+ addItem(container, "Call of Duty", "Games");
+ addItem(container, "Might and Magic", "Games");
+ addItem(container, "Fallout", "Games");
+ addItem(container, "Red Alert", "Games");
+
+ addItem(container, "Cars", null);
+ addItem(container, "Toyota", "Cars");
+ addItem(container, "Volvo", "Cars");
+ addItem(container, "Audi", "Cars");
+ addItem(container, "Ford", "Cars");
+
+ addItem(container, "Natural languages", null);
+ addItem(container, "Swedish", "Natural languages");
+ addItem(container, "English", "Natural languages");
+ addItem(container, "Finnish", "Natural languages");
+
+ addItem(container, "Programming languages", null);
+ addItem(container, "C++", "Programming languages");
+ addItem(container, "PHP", "Programming languages");
+ addItem(container, "Java", "Programming languages");
+ addItem(container, "Python", "Programming languages");
+
+ }
+
+ private static int index = 0;
+ private static Map<String, Integer> nameToId = new HashMap<String, Integer>();
+ private static Map<Integer, String> idToName = new HashMap<Integer, String>();
+
+ public static void addItem(IndexedContainer container, String string,
+ String parent) {
+ nameToId.put(string, index);
+ idToName.put(index, string);
+
+ Item item = container.addItem(index);
+ item.getItemProperty("name").setValue(string);
+
+ if (parent != null && container instanceof HierarchicalContainer) {
+ ((HierarchicalContainer) container).setParent(index,
+ nameToId.get(parent));
+ }
+
+ index++;
+ }
+
+ private void verifyOrder(Container.Sortable ic, Object[] idOrder) {
+ int size = ic.size();
+ Object[] actual = new Object[size];
+ Iterator<?> i = ic.getItemIds().iterator();
+ int index = 0;
+ while (i.hasNext()) {
+ Object o = i.next();
+ if (o.getClass() == Integer.class
+ && idOrder[index].getClass() == String.class) {
+ o = idToName.get(o);
+ }
+ actual[index++] = o;
+ }
+
+ TestUtil.assertArrays(actual, idOrder);
+
+ }
+
+ private void populate(IndexedContainer ic) {
+ addItem(ic, ITEM_STRING_1, ITEM_STRING_1, 1, 1);
+ addItem(ic, ITEM_STRING_NULL2, null, 0, null);
+ addItem(ic, ITEM_STRING_2, ITEM_STRING_2, 2, 2);
+ addItem(ic, ITEM_ANOTHER_NULL, null, 0, null);
+ addItem(ic, ITEM_DATA_MINUS1, ITEM_DATA_MINUS1, -1, -1);
+ addItem(ic, ITEM_DATA_MINUS1_NULL, null, -1, null);
+ addItem(ic, ITEM_DATA_MINUS2, ITEM_DATA_MINUS2, -2, -2);
+ addItem(ic, ITEM_DATA_MINUS2_NULL, null, -2, null);
+ }
+
+ private Item addItem(Container ic, String id, String string_null,
+ int integer, Integer integer_null) {
+ Item i = ic.addItem(id);
+ i.getItemProperty(PROPERTY_STRING_ID).setValue(id);
+ i.getItemProperty(PROPERTY_STRING_NULL).setValue(string_null);
+ i.getItemProperty(PROPERTY_INTEGER_NOT_NULL).setValue(integer);
+ i.getItemProperty(PROPERTY_INTEGER_NULL2).setValue(integer_null);
+
+ return i;
+ }
+
+ private void addProperties(IndexedContainer ic) {
+ ic.addContainerProperty("id", String.class, null);
+ ic.addContainerProperty(PROPERTY_STRING_ID, String.class, "");
+ ic.addContainerProperty(PROPERTY_STRING_NULL, String.class, null);
+ ic.addContainerProperty(PROPERTY_INTEGER_NULL2, Integer.class, null);
+ ic.addContainerProperty(PROPERTY_INTEGER_NOT_NULL, Integer.class, 0);
+ ic.addContainerProperty("comparable-null", Integer.class, 0);
+ }
+
+ public class MyObject implements Comparable<MyObject> {
+ private String data;
+
+ @Override
+ public int compareTo(MyObject o) {
+ if (o == null) {
+ return 1;
+ }
+
+ if (o.data == null) {
+ return data == null ? 0 : 1;
+ } else if (data == null) {
+ return -1;
+ } else {
+ return data.compareTo(o.data);
+ }
+ }
+ }
+
+}
diff --git a/compatibility-server/src/test/java/com/vaadin/data/util/FileSystemContainerTest.java b/compatibility-server/src/test/java/com/vaadin/data/util/FileSystemContainerTest.java
new file mode 100644
index 0000000000..992b265702
--- /dev/null
+++ b/compatibility-server/src/test/java/com/vaadin/data/util/FileSystemContainerTest.java
@@ -0,0 +1,16 @@
+package com.vaadin.data.util;
+
+import java.io.File;
+
+import org.junit.Assert;
+import org.junit.Test;
+
+public class FileSystemContainerTest {
+
+ @Test
+ public void nonExistingDirectory() {
+ FilesystemContainer fsc = new FilesystemContainer(
+ new File("/non/existing"));
+ Assert.assertTrue(fsc.getItemIds().isEmpty());
+ }
+}
diff --git a/compatibility-server/src/test/java/com/vaadin/data/util/GeneratedPropertyContainerBasicTest.java b/compatibility-server/src/test/java/com/vaadin/data/util/GeneratedPropertyContainerBasicTest.java
new file mode 100644
index 0000000000..11335314f0
--- /dev/null
+++ b/compatibility-server/src/test/java/com/vaadin/data/util/GeneratedPropertyContainerBasicTest.java
@@ -0,0 +1,596 @@
+package com.vaadin.data.util;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.fail;
+
+import java.util.List;
+
+import org.easymock.Capture;
+import org.easymock.EasyMock;
+import org.junit.Assert;
+import org.junit.Test;
+
+import com.vaadin.data.Container;
+import com.vaadin.data.Container.Indexed.ItemAddEvent;
+import com.vaadin.data.Container.Indexed.ItemRemoveEvent;
+import com.vaadin.data.Container.ItemSetChangeListener;
+import com.vaadin.data.Container.ItemSetChangeNotifier;
+import com.vaadin.data.Item;
+import com.vaadin.data.util.filter.SimpleStringFilter;
+
+public class GeneratedPropertyContainerBasicTest
+ extends AbstractInMemoryContainerTestBase {
+
+ @Test
+ public void testBasicOperations() {
+ testBasicContainerOperations(createContainer());
+ }
+
+ private GeneratedPropertyContainer createContainer() {
+ return new GeneratedPropertyContainer(new IndexedContainer());
+ }
+
+ @Test
+ public void testFiltering() {
+ testContainerFiltering(createContainer());
+ }
+
+ @Test
+ public void testSorting() {
+ testContainerSorting(createContainer());
+ }
+
+ @Test
+ public void testSortingAndFiltering() {
+ testContainerSortingAndFiltering(createContainer());
+ }
+
+ @Test
+ public void testContainerOrdered() {
+ testContainerOrdered(createContainer());
+ }
+
+ @Test
+ public void testContainerIndexed() {
+ testContainerIndexed(createContainer(), sampleData[2], 2, true,
+ "newItemId", true);
+ }
+
+ @Test
+ public void testItemSetChangeListeners() {
+ GeneratedPropertyContainer container = createContainer();
+ ItemSetChangeCounter counter = new ItemSetChangeCounter();
+ container.addListener(counter);
+
+ String id1 = "id1";
+ String id2 = "id2";
+ String id3 = "id3";
+
+ initializeContainer(container);
+ counter.reset();
+ container.addItem();
+ counter.assertOnce();
+ container.addItem(id1);
+ counter.assertOnce();
+
+ initializeContainer(container);
+ counter.reset();
+ container.addItemAt(0);
+ counter.assertOnce();
+ container.addItemAt(0, id1);
+ counter.assertOnce();
+ container.addItemAt(0, id2);
+ counter.assertOnce();
+ container.addItemAt(container.size(), id3);
+ counter.assertOnce();
+ // no notification if already in container
+ container.addItemAt(0, id1);
+ counter.assertNone();
+
+ initializeContainer(container);
+ counter.reset();
+ container.addItemAfter(null);
+ counter.assertOnce();
+ container.addItemAfter(null, id1);
+ counter.assertOnce();
+ container.addItemAfter(id1);
+ counter.assertOnce();
+ container.addItemAfter(id1, id2);
+ counter.assertOnce();
+ container.addItemAfter(container.firstItemId());
+ counter.assertOnce();
+ container.addItemAfter(container.lastItemId());
+ counter.assertOnce();
+ container.addItemAfter(container.lastItemId(), id3);
+ counter.assertOnce();
+ // no notification if already in container
+ container.addItemAfter(0, id1);
+ counter.assertNone();
+
+ initializeContainer(container);
+ counter.reset();
+ container.removeItem(sampleData[0]);
+ counter.assertOnce();
+
+ initializeContainer(container);
+ counter.reset();
+ // no notification for removing a non-existing item
+ container.removeItem(id1);
+ counter.assertNone();
+
+ initializeContainer(container);
+ counter.reset();
+ container.removeAllItems();
+ counter.assertOnce();
+ // already empty
+ container.removeAllItems();
+ counter.assertNone();
+
+ }
+
+ @Test
+ public void testAddRemoveContainerFilter() {
+ GeneratedPropertyContainer container = createContainer();
+ ItemSetChangeCounter counter = new ItemSetChangeCounter();
+ container.addListener(counter);
+
+ // simply adding or removing container filters should cause events
+ // (content changes)
+
+ initializeContainer(container);
+ counter.reset();
+ SimpleStringFilter filter = new SimpleStringFilter(SIMPLE_NAME, "a",
+ true, false);
+ container.addContainerFilter(filter);
+ counter.assertOnce();
+ container.removeContainerFilter(filter);
+ counter.assertOnce();
+ container.addContainerFilter(
+ new SimpleStringFilter(SIMPLE_NAME, "a", true, false));
+ counter.assertOnce();
+ container.removeAllContainerFilters();
+ counter.assertOnce();
+ }
+
+ // TODO other tests should check positions after removing filter etc,
+ // here concentrating on listeners
+ @Test
+ public void testItemSetChangeListenersFiltering() {
+ Container.Indexed container = createContainer();
+ ItemSetChangeCounter counter = new ItemSetChangeCounter();
+ ((GeneratedPropertyContainer) container).addListener(counter);
+
+ counter.reset();
+ ((Container.Filterable) container)
+ .addContainerFilter(new SimpleStringFilter(FULLY_QUALIFIED_NAME,
+ "Test", true, false));
+ // no real change, so no notification required
+ counter.assertNone();
+
+ String id1 = "com.example.Test1";
+ String id2 = "com.example.Test2";
+ String id3 = "com.example.Other";
+
+ // perform operations while filtering container
+
+ Item item;
+
+ initializeContainer(container);
+ counter.reset();
+ // passes filter
+ item = container.addItem(id1);
+ // no event if filtered out
+ counter.assertNone();
+ item.getItemProperty(FULLY_QUALIFIED_NAME).setValue(id1);
+ counter.assertOnce();
+ // passes filter but already in the container
+ item = container.addItem(id1);
+ counter.assertNone();
+
+ initializeContainer(container);
+ counter.reset();
+ // passes filter after change
+ item = container.addItemAt(0, id1);
+ counter.assertNone();
+ item.getItemProperty(FULLY_QUALIFIED_NAME).setValue(id1);
+ counter.assertOnce();
+ item = container.addItemAt(container.size(), id2);
+ counter.assertNone();
+ item.getItemProperty(FULLY_QUALIFIED_NAME).setValue(id2);
+ counter.assertOnce();
+ // passes filter but already in the container
+ item = container.addItemAt(0, id1);
+ counter.assertNone();
+ item = container.addItemAt(container.size(), id2);
+ counter.assertNone();
+
+ initializeContainer(container);
+ counter.reset();
+ // passes filter
+ item = container.addItemAfter(null, id1);
+ counter.assertNone();
+ item.getItemProperty(FULLY_QUALIFIED_NAME).setValue(id1);
+ counter.assertOnce();
+ item = container.addItemAfter(container.lastItemId(), id2);
+ counter.assertNone();
+ item.getItemProperty(FULLY_QUALIFIED_NAME).setValue(id2);
+ counter.assertOnce();
+ // passes filter but already in the container
+ item = container.addItemAfter(null, id1);
+ counter.assertNone();
+ item = container.addItemAfter(container.lastItemId(), id2);
+ counter.assertNone();
+
+ // does not pass filter
+
+ // TODO implement rest
+
+ initializeContainer(container);
+ counter.reset();
+ item = container.addItemAfter(null, id3);
+ counter.assertNone();
+ item.getItemProperty(FULLY_QUALIFIED_NAME).setValue(id3);
+ counter.assertNone();
+
+ initializeContainer(container);
+ counter.reset();
+ item = container.addItemAfter(container.firstItemId(), id3);
+ counter.assertNone();
+ item.getItemProperty(FULLY_QUALIFIED_NAME).setValue(id3);
+ counter.assertNone();
+
+ initializeContainer(container);
+ counter.reset();
+ item = container.addItemAfter(container.lastItemId(), id3);
+ counter.assertNone();
+ item.getItemProperty(FULLY_QUALIFIED_NAME).setValue(id3);
+ counter.assertNone();
+
+ initializeContainer(container);
+ counter.reset();
+ item = container.addItemAt(0, id3);
+ counter.assertNone();
+ item.getItemProperty(FULLY_QUALIFIED_NAME).setValue(id3);
+ counter.assertNone();
+
+ initializeContainer(container);
+ counter.reset();
+ item = container.addItemAt(1, id3);
+ counter.assertNone();
+ item.getItemProperty(FULLY_QUALIFIED_NAME).setValue(id3);
+ counter.assertNone();
+
+ initializeContainer(container);
+ counter.reset();
+ item = container.addItemAt(container.size(), id3);
+ counter.assertNone();
+ item.getItemProperty(FULLY_QUALIFIED_NAME).setValue(id3);
+ counter.assertNone();
+
+ // passes filter
+
+ initializeContainer(container);
+ counter.reset();
+ item = container.addItem(id1);
+ counter.assertNone();
+ item.getItemProperty(FULLY_QUALIFIED_NAME).setValue(id1);
+ counter.assertOnce();
+ container.removeItem(id1);
+ counter.assertOnce();
+ // already removed
+ container.removeItem(id1);
+ counter.assertNone();
+
+ item = container.addItem(id3);
+ counter.assertNone();
+ item.getItemProperty(FULLY_QUALIFIED_NAME).setValue(id3);
+ counter.assertNone();
+ // not visible
+ container.removeItem(id3);
+ counter.assertNone();
+
+ // remove all
+
+ initializeContainer(container);
+ item = container.addItem(id1);
+ item.getItemProperty(FULLY_QUALIFIED_NAME).setValue(id1);
+ counter.reset();
+ container.removeAllItems();
+ counter.assertOnce();
+ // no visible items
+ container.removeAllItems();
+ counter.assertNone();
+ }
+
+ @Test
+ public void testItemAdd_idSequence() {
+ GeneratedPropertyContainer container = createContainer();
+ Object itemId;
+
+ itemId = container.addItem();
+ assertEquals(Integer.valueOf(1), itemId);
+
+ itemId = container.addItem();
+ assertEquals(Integer.valueOf(2), itemId);
+
+ itemId = container.addItemAfter(null);
+ assertEquals(Integer.valueOf(3), itemId);
+
+ itemId = container.addItemAt(2);
+ assertEquals(Integer.valueOf(4), itemId);
+ }
+
+ @Test
+ public void testItemAddRemove_idSequence() {
+ GeneratedPropertyContainer container = createContainer();
+ Object itemId;
+
+ itemId = container.addItem();
+ assertEquals(Integer.valueOf(1), itemId);
+
+ container.removeItem(itemId);
+
+ itemId = container.addItem();
+ assertEquals(
+ "Id sequence should continue from the previous value even if an item is removed",
+ Integer.valueOf(2), itemId);
+ }
+
+ @Test
+ public void testItemAddedEvent() {
+ GeneratedPropertyContainer container = createContainer();
+ ItemSetChangeListener addListener = createListenerMockFor(container);
+ addListener.containerItemSetChange(EasyMock.isA(ItemAddEvent.class));
+ EasyMock.replay(addListener);
+
+ container.addItem();
+
+ EasyMock.verify(addListener);
+ }
+
+ @Test
+ public void testItemAddedEvent_AddedItem() {
+ GeneratedPropertyContainer container = createContainer();
+ ItemSetChangeListener addListener = createListenerMockFor(container);
+ Capture<ItemAddEvent> capturedEvent = captureAddEvent(addListener);
+ EasyMock.replay(addListener);
+
+ Object itemId = container.addItem();
+
+ assertEquals(itemId, capturedEvent.getValue().getFirstItemId());
+ }
+
+ @Test
+ public void testItemAddedEvent_IndexOfAddedItem() {
+ GeneratedPropertyContainer container = createContainer();
+ ItemSetChangeListener addListener = createListenerMockFor(container);
+ container.addItem();
+ Capture<ItemAddEvent> capturedEvent = captureAddEvent(addListener);
+ EasyMock.replay(addListener);
+
+ Object itemId = container.addItemAt(1);
+
+ assertEquals(1, capturedEvent.getValue().getFirstIndex());
+ }
+
+ @Test
+ public void testItemRemovedEvent() {
+ GeneratedPropertyContainer container = createContainer();
+ Object itemId = container.addItem();
+ ItemSetChangeListener removeListener = createListenerMockFor(container);
+ removeListener
+ .containerItemSetChange(EasyMock.isA(ItemRemoveEvent.class));
+ EasyMock.replay(removeListener);
+
+ container.removeItem(itemId);
+
+ EasyMock.verify(removeListener);
+ }
+
+ @Test
+ public void testItemRemovedEvent_RemovedItem() {
+ GeneratedPropertyContainer container = createContainer();
+ Object itemId = container.addItem();
+ ItemSetChangeListener removeListener = createListenerMockFor(container);
+ Capture<ItemRemoveEvent> capturedEvent = captureRemoveEvent(
+ removeListener);
+ EasyMock.replay(removeListener);
+
+ container.removeItem(itemId);
+
+ assertEquals(itemId, capturedEvent.getValue().getFirstItemId());
+ }
+
+ @Test
+ public void testItemRemovedEvent_indexOfRemovedItem() {
+ GeneratedPropertyContainer container = createContainer();
+ container.addItem();
+ Object secondItemId = container.addItem();
+ ItemSetChangeListener removeListener = createListenerMockFor(container);
+ Capture<ItemRemoveEvent> capturedEvent = captureRemoveEvent(
+ removeListener);
+ EasyMock.replay(removeListener);
+
+ container.removeItem(secondItemId);
+
+ assertEquals(1, capturedEvent.getValue().getFirstIndex());
+ }
+
+ @Test
+ public void testItemRemovedEvent_amountOfRemovedItems() {
+ GeneratedPropertyContainer container = createContainer();
+ container.addItem();
+ container.addItem();
+ ItemSetChangeListener removeListener = createListenerMockFor(container);
+ Capture<ItemRemoveEvent> capturedEvent = captureRemoveEvent(
+ removeListener);
+ EasyMock.replay(removeListener);
+
+ container.removeAllItems();
+
+ assertEquals(2, capturedEvent.getValue().getRemovedItemsCount());
+ }
+
+ private Capture<ItemAddEvent> captureAddEvent(
+ ItemSetChangeListener addListener) {
+ Capture<ItemAddEvent> capturedEvent = new Capture<ItemAddEvent>();
+ addListener.containerItemSetChange(EasyMock.capture(capturedEvent));
+ return capturedEvent;
+ }
+
+ private Capture<ItemRemoveEvent> captureRemoveEvent(
+ ItemSetChangeListener removeListener) {
+ Capture<ItemRemoveEvent> capturedEvent = new Capture<ItemRemoveEvent>();
+ removeListener.containerItemSetChange(EasyMock.capture(capturedEvent));
+ return capturedEvent;
+ }
+
+ private ItemSetChangeListener createListenerMockFor(
+ ItemSetChangeNotifier container) {
+ ItemSetChangeListener listener = EasyMock
+ .createNiceMock(ItemSetChangeListener.class);
+ container.addItemSetChangeListener(listener);
+ return listener;
+ }
+
+ // Ticket 8028
+ @Test
+ public void testGetItemIdsRangeIndexOutOfBounds() {
+ GeneratedPropertyContainer ic = createContainer();
+ try {
+ ic.getItemIds(-1, 10);
+ fail("Container returned items starting from index -1, something very wrong here!");
+ } catch (IndexOutOfBoundsException e) {
+ // This is expected...
+ } catch (Exception e) {
+ // Should not happen!
+ fail("Container threw unspecified exception when fetching a range of items and the range started from -1");
+ }
+
+ }
+
+ // Ticket 8028
+ @Test
+ public void testGetItemIdsRangeIndexOutOfBounds2() {
+ GeneratedPropertyContainer ic = createContainer();
+ ic.addItem(new Object());
+ try {
+ ic.getItemIds(2, 1);
+ fail("Container returned items starting from index -1, something very wrong here!");
+ } catch (IndexOutOfBoundsException e) {
+ // This is expected...
+ } catch (Exception e) {
+ // Should not happen!
+ fail("Container threw unspecified exception when fetching a out of bounds range of items");
+ }
+
+ }
+
+ // Ticket 8028
+ @Test
+ public void testGetItemIdsRangeZeroRange() {
+ GeneratedPropertyContainer ic = createContainer();
+ ic.addItem(new Object());
+ try {
+ List<Object> itemIds = (List<Object>) ic.getItemIds(1, 0);
+
+ assertTrue(
+ "Container returned actual values when asking for 0 items...",
+ itemIds.isEmpty());
+ } catch (Exception e) {
+ // Should not happen!
+ fail("Container threw unspecified exception when fetching 0 items...");
+ }
+
+ }
+
+ // Ticket 8028
+ @Test
+ public void testGetItemIdsRangeNegativeRange() {
+ GeneratedPropertyContainer ic = createContainer();
+ ic.addItem(new Object());
+ try {
+ List<Object> itemIds = (List<Object>) ic.getItemIds(1, -1);
+
+ assertTrue(
+ "Container returned actual values when asking for -1 items...",
+ itemIds.isEmpty());
+ } catch (IllegalArgumentException e) {
+ // this is expected
+
+ } catch (Exception e) {
+ // Should not happen!
+ fail("Container threw unspecified exception when fetching -1 items...");
+ }
+
+ }
+
+ // Ticket 8028
+ @Test
+ public void testGetItemIdsRangeIndexOutOfBoundsDueToSizeChange() {
+ GeneratedPropertyContainer ic = createContainer();
+ ic.addItem(new Object());
+ Assert.assertEquals(
+ "Container returned too many items when the range was >> container size",
+ 1, ic.getItemIds(0, 10).size());
+ }
+
+ // Ticket 8028
+ @Test
+ public void testGetItemIdsRangeBaseCase() {
+ GeneratedPropertyContainer ic = createContainer();
+ String object1 = new String("Obj1");
+ String object2 = new String("Obj2");
+ String object3 = new String("Obj3");
+ String object4 = new String("Obj4");
+ String object5 = new String("Obj5");
+
+ ic.addItem(object1);
+ ic.addItem(object2);
+ ic.addItem(object3);
+ ic.addItem(object4);
+ ic.addItem(object5);
+
+ try {
+ List<Object> itemIds = (List<Object>) ic.getItemIds(1, 2);
+
+ assertTrue(itemIds.contains(object2));
+ assertTrue(itemIds.contains(object3));
+ assertEquals(2, itemIds.size());
+
+ } catch (Exception e) {
+ // Should not happen!
+ fail("Container threw exception when fetching a range of items ");
+ }
+ }
+
+ // test getting non-existing property (#10445)
+ @Test
+ public void testNonExistingProperty() {
+ Container ic = createContainer();
+ String object1 = new String("Obj1");
+ ic.addItem(object1);
+ assertNull(ic.getContainerProperty(object1, "xyz"));
+ }
+
+ // test getting null property id (#10445)
+ @Test
+ public void testNullPropertyId() {
+ Container ic = createContainer();
+ String object1 = new String("Obj1");
+ ic.addItem(object1);
+ assertNull(ic.getContainerProperty(object1, null));
+ }
+
+ @Override
+ protected void initializeContainer(Container container) {
+ if (container instanceof GeneratedPropertyContainer) {
+ super.initializeContainer(((GeneratedPropertyContainer) container)
+ .getWrappedContainer());
+ } else {
+ super.initializeContainer(container);
+ }
+ }
+}
diff --git a/compatibility-server/src/test/java/com/vaadin/data/util/GeneratedPropertyContainerTest.java b/compatibility-server/src/test/java/com/vaadin/data/util/GeneratedPropertyContainerTest.java
new file mode 100644
index 0000000000..1e34567439
--- /dev/null
+++ b/compatibility-server/src/test/java/com/vaadin/data/util/GeneratedPropertyContainerTest.java
@@ -0,0 +1,318 @@
+/*
+ * Copyright 2000-2016 Vaadin Ltd.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not
+ * use this file except in compliance with the License. You may obtain a copy of
+ * the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+package com.vaadin.data.util;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.fail;
+
+import org.junit.Before;
+import org.junit.Test;
+
+import com.vaadin.data.Container.Filter;
+import com.vaadin.data.Container.Indexed;
+import com.vaadin.data.Container.ItemSetChangeEvent;
+import com.vaadin.data.Container.ItemSetChangeListener;
+import com.vaadin.data.Container.PropertySetChangeEvent;
+import com.vaadin.data.Container.PropertySetChangeListener;
+import com.vaadin.data.Item;
+import com.vaadin.data.sort.SortOrder;
+import com.vaadin.data.util.GeneratedPropertyContainer.GeneratedPropertyItem;
+import com.vaadin.data.util.filter.Compare;
+import com.vaadin.data.util.filter.UnsupportedFilterException;
+
+public class GeneratedPropertyContainerTest {
+
+ GeneratedPropertyContainer container;
+ Indexed wrappedContainer;
+ private static double MILES_CONVERSION = 0.6214d;
+
+ private class GeneratedPropertyListener
+ implements PropertySetChangeListener {
+
+ private int callCount = 0;
+
+ public int getCallCount() {
+ return callCount;
+ }
+
+ @Override
+ public void containerPropertySetChange(PropertySetChangeEvent event) {
+ ++callCount;
+ assertEquals(
+ "Container for event was not GeneratedPropertyContainer",
+ event.getContainer(), container);
+ }
+ }
+
+ private class GeneratedItemSetListener implements ItemSetChangeListener {
+
+ private int callCount = 0;
+
+ public int getCallCount() {
+ return callCount;
+ }
+
+ @Override
+ public void containerItemSetChange(ItemSetChangeEvent event) {
+ ++callCount;
+ assertEquals(
+ "Container for event was not GeneratedPropertyContainer",
+ event.getContainer(), container);
+ }
+ }
+
+ @Before
+ public void setUp() {
+ container = new GeneratedPropertyContainer(createContainer());
+ }
+
+ @Test
+ public void testSimpleGeneratedProperty() {
+ container.addGeneratedProperty("hello",
+ new PropertyValueGenerator<String>() {
+
+ @Override
+ public String getValue(Item item, Object itemId,
+ Object propertyId) {
+ return "Hello World!";
+ }
+
+ @Override
+ public Class<String> getType() {
+ return String.class;
+ }
+ });
+
+ Object itemId = container.addItem();
+ assertEquals("Expected value not in item.",
+ container.getItem(itemId).getItemProperty("hello").getValue(),
+ "Hello World!");
+ }
+
+ @Test
+ public void testSortableProperties() {
+ container.addGeneratedProperty("baz",
+ new PropertyValueGenerator<String>() {
+
+ @Override
+ public String getValue(Item item, Object itemId,
+ Object propertyId) {
+ return item.getItemProperty("foo").getValue() + " "
+ + item.getItemProperty("bar").getValue();
+ }
+
+ @Override
+ public Class<String> getType() {
+ return String.class;
+ }
+
+ @Override
+ public SortOrder[] getSortProperties(SortOrder order) {
+ SortOrder[] sortOrder = new SortOrder[1];
+ sortOrder[0] = new SortOrder("bar",
+ order.getDirection());
+ return sortOrder;
+ }
+ });
+
+ container.sort(new Object[] { "baz" }, new boolean[] { true });
+ assertEquals("foo 0", container.getItem(container.getIdByIndex(0))
+ .getItemProperty("baz").getValue());
+
+ container.sort(new Object[] { "baz" }, new boolean[] { false });
+ assertEquals("foo 10", container.getItem(container.getIdByIndex(0))
+ .getItemProperty("baz").getValue());
+ }
+
+ @Test
+ public void testOverrideSortableProperties() {
+
+ assertTrue(container.getSortableContainerPropertyIds().contains("bar"));
+
+ container.addGeneratedProperty("bar",
+ new PropertyValueGenerator<String>() {
+
+ @Override
+ public String getValue(Item item, Object itemId,
+ Object propertyId) {
+ return item.getItemProperty("foo").getValue() + " "
+ + item.getItemProperty("bar").getValue();
+ }
+
+ @Override
+ public Class<String> getType() {
+ return String.class;
+ }
+ });
+
+ assertFalse(
+ container.getSortableContainerPropertyIds().contains("bar"));
+ }
+
+ @Test
+ public void testFilterByMiles() {
+ container.addGeneratedProperty("miles",
+ new PropertyValueGenerator<Double>() {
+
+ @Override
+ public Double getValue(Item item, Object itemId,
+ Object propertyId) {
+ return (Double) item.getItemProperty("km").getValue()
+ * MILES_CONVERSION;
+ }
+
+ @Override
+ public Class<Double> getType() {
+ return Double.class;
+ }
+
+ @Override
+ public Filter modifyFilter(Filter filter)
+ throws UnsupportedFilterException {
+ if (filter instanceof Compare.LessOrEqual) {
+ Double value = (Double) ((Compare.LessOrEqual) filter)
+ .getValue();
+ value = value / MILES_CONVERSION;
+ return new Compare.LessOrEqual("km", value);
+ }
+ return super.modifyFilter(filter);
+ }
+ });
+
+ for (Object itemId : container.getItemIds()) {
+ Item item = container.getItem(itemId);
+ Double km = (Double) item.getItemProperty("km").getValue();
+ Double miles = (Double) item.getItemProperty("miles").getValue();
+ assertTrue(miles.equals(km * MILES_CONVERSION));
+ }
+
+ Filter filter = new Compare.LessOrEqual("miles", MILES_CONVERSION);
+ container.addContainerFilter(filter);
+ for (Object itemId : container.getItemIds()) {
+ Item item = container.getItem(itemId);
+ assertTrue("Item did not pass original filter.",
+ filter.passesFilter(itemId, item));
+ }
+
+ assertTrue(container.getContainerFilters().contains(filter));
+ container.removeContainerFilter(filter);
+ assertFalse(container.getContainerFilters().contains(filter));
+
+ boolean allPass = true;
+ for (Object itemId : container.getItemIds()) {
+ Item item = container.getItem(itemId);
+ if (!filter.passesFilter(itemId, item)) {
+ allPass = false;
+ }
+ }
+
+ if (allPass) {
+ fail("Removing filter did not introduce any previous filtered items");
+ }
+ }
+
+ @Test
+ public void testPropertySetChangeNotifier() {
+ GeneratedPropertyListener listener = new GeneratedPropertyListener();
+ GeneratedPropertyListener removedListener = new GeneratedPropertyListener();
+ container.addPropertySetChangeListener(listener);
+ container.addPropertySetChangeListener(removedListener);
+
+ container.addGeneratedProperty("foo",
+ new PropertyValueGenerator<String>() {
+
+ @Override
+ public String getValue(Item item, Object itemId,
+ Object propertyId) {
+ return "";
+ }
+
+ @Override
+ public Class<String> getType() {
+ return String.class;
+ }
+ });
+
+ // Adding property to wrapped container should cause an event
+ wrappedContainer.addContainerProperty("baz", String.class, "");
+ container.removePropertySetChangeListener(removedListener);
+ container.removeGeneratedProperty("foo");
+
+ assertEquals("Listener was not called correctly.", 3,
+ listener.getCallCount());
+ assertEquals("Removed listener was not called correctly.", 2,
+ removedListener.getCallCount());
+ }
+
+ @Test
+ public void testItemSetChangeNotifier() {
+ GeneratedItemSetListener listener = new GeneratedItemSetListener();
+ container.addItemSetChangeListener(listener);
+
+ container.sort(new Object[] { "foo" }, new boolean[] { true });
+ container.sort(new Object[] { "foo" }, new boolean[] { false });
+
+ assertEquals("Listener was not called correctly.", 2,
+ listener.getCallCount());
+
+ }
+
+ @Test
+ public void testRemoveProperty() {
+ container.removeContainerProperty("foo");
+ assertFalse("Container contained removed property",
+ container.getContainerPropertyIds().contains("foo"));
+ assertTrue("Wrapped container did not contain removed property",
+ wrappedContainer.getContainerPropertyIds().contains("foo"));
+
+ assertFalse(container.getItem(container.firstItemId())
+ .getItemPropertyIds().contains("foo"));
+
+ container.addContainerProperty("foo", null, null);
+ assertTrue("Container did not contain returned property",
+ container.getContainerPropertyIds().contains("foo"));
+ }
+
+ @Test
+ public void testGetWrappedItem() {
+ Object itemId = wrappedContainer.getItemIds().iterator().next();
+ Item wrappedItem = wrappedContainer.getItem(itemId);
+ GeneratedPropertyItem generatedPropertyItem = (GeneratedPropertyItem) container
+ .getItem(itemId);
+ assertEquals(wrappedItem, generatedPropertyItem.getWrappedItem());
+ }
+
+ private Indexed createContainer() {
+ wrappedContainer = new IndexedContainer();
+ wrappedContainer.addContainerProperty("foo", String.class, "foo");
+ wrappedContainer.addContainerProperty("bar", Integer.class, 0);
+ // km contains double values from 0.0 to 2.0
+ wrappedContainer.addContainerProperty("km", Double.class, 0);
+
+ for (int i = 0; i <= 10; ++i) {
+ Object itemId = wrappedContainer.addItem();
+ Item item = wrappedContainer.getItem(itemId);
+ item.getItemProperty("foo").setValue("foo");
+ item.getItemProperty("bar").setValue(i);
+ item.getItemProperty("km").setValue(i / 5.0d);
+ }
+
+ return wrappedContainer;
+ }
+
+}
diff --git a/compatibility-server/src/test/java/com/vaadin/data/util/HierarchicalContainerOrderedWrapperTest.java b/compatibility-server/src/test/java/com/vaadin/data/util/HierarchicalContainerOrderedWrapperTest.java
new file mode 100644
index 0000000000..54984f07ad
--- /dev/null
+++ b/compatibility-server/src/test/java/com/vaadin/data/util/HierarchicalContainerOrderedWrapperTest.java
@@ -0,0 +1,33 @@
+package com.vaadin.data.util;
+
+import org.junit.Test;
+
+public class HierarchicalContainerOrderedWrapperTest
+ extends AbstractHierarchicalContainerTestBase {
+
+ private HierarchicalContainerOrderedWrapper createContainer() {
+ return new HierarchicalContainerOrderedWrapper(
+ new ContainerHierarchicalWrapper(new IndexedContainer()));
+ }
+
+ @Test
+ public void testBasicOperations() {
+ testBasicContainerOperations(createContainer());
+ }
+
+ @Test
+ public void testHierarchicalContainer() {
+ testHierarchicalContainer(createContainer());
+ }
+
+ @Test
+ public void testContainerOrdered() {
+ testContainerOrdered(createContainer());
+ }
+
+ @Test
+ public void testRemoveSubtree() {
+ testRemoveHierarchicalWrapperSubtree(createContainer());
+ }
+
+}
diff --git a/compatibility-server/src/test/java/com/vaadin/data/util/HierarchicalContainerTest.java b/compatibility-server/src/test/java/com/vaadin/data/util/HierarchicalContainerTest.java
new file mode 100644
index 0000000000..7ab20ca3dd
--- /dev/null
+++ b/compatibility-server/src/test/java/com/vaadin/data/util/HierarchicalContainerTest.java
@@ -0,0 +1,286 @@
+package com.vaadin.data.util;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNull;
+
+import org.junit.Test;
+
+import com.vaadin.data.Container.Filter;
+import com.vaadin.data.Item;
+
+public class HierarchicalContainerTest
+ extends AbstractHierarchicalContainerTestBase {
+
+ @Test
+ public void testBasicOperations() {
+ testBasicContainerOperations(new HierarchicalContainer());
+ }
+
+ @Test
+ public void testFiltering() {
+ testContainerFiltering(new HierarchicalContainer());
+ }
+
+ @Test
+ public void testSorting() {
+ testContainerSorting(new HierarchicalContainer());
+ }
+
+ @Test
+ public void testOrdered() {
+ testContainerOrdered(new HierarchicalContainer());
+ }
+
+ @Test
+ public void testHierarchicalSorting() {
+ testHierarchicalSorting(new HierarchicalContainer());
+ }
+
+ @Test
+ public void testSortingAndFiltering() {
+ testContainerSortingAndFiltering(new HierarchicalContainer());
+ }
+
+ @Test
+ public void testRemovingItemsFromFilteredContainer() {
+ HierarchicalContainer container = new HierarchicalContainer();
+ initializeContainer(container);
+ container.setIncludeParentsWhenFiltering(true);
+ container.addContainerFilter(FULLY_QUALIFIED_NAME, "ab", false, false);
+ Object p1 = container.getParent("com.vaadin.ui.TabSheet");
+ assertEquals("com.vaadin.ui", p1);
+
+ container.removeItem("com.vaadin.ui.TabSheet");
+ // Parent for the removed item must be null because the item is no
+ // longer in the container
+ p1 = container.getParent("com.vaadin.ui.TabSheet");
+ assertNull("Parent should be null, is " + p1, p1);
+
+ container.removeAllItems();
+ p1 = container.getParent("com.vaadin.terminal.gwt.client.Focusable");
+ assertNull("Parent should be null, is " + p1, p1);
+
+ }
+
+ @Test
+ public void testParentWhenRemovingFilterFromContainer() {
+ HierarchicalContainer container = new HierarchicalContainer();
+ initializeContainer(container);
+ container.setIncludeParentsWhenFiltering(true);
+ container.addContainerFilter(FULLY_QUALIFIED_NAME, "ab", false, false);
+ Object p1 = container.getParent("com.vaadin.ui.TabSheet");
+ assertEquals("com.vaadin.ui", p1);
+ p1 = container
+ .getParent("com.vaadin.terminal.gwt.client.ui.VPopupCalendar");
+ assertNull(p1);
+ container.removeAllContainerFilters();
+ p1 = container
+ .getParent("com.vaadin.terminal.gwt.client.ui.VPopupCalendar");
+ assertEquals("com.vaadin.terminal.gwt.client.ui", p1);
+
+ }
+
+ @Test
+ public void testChangeParentInFilteredContainer() {
+ HierarchicalContainer container = new HierarchicalContainer();
+ initializeContainer(container);
+ container.setIncludeParentsWhenFiltering(true);
+ container.addContainerFilter(FULLY_QUALIFIED_NAME, "Tab", false, false);
+
+ // Change parent of filtered item
+ Object p1 = container.getParent("com.vaadin.ui.TabSheet");
+ assertEquals("com.vaadin.ui", p1);
+ container.setParent("com.vaadin.ui.TabSheet", "com.vaadin");
+ p1 = container.getParent("com.vaadin.ui.TabSheet");
+ assertEquals("com.vaadin", p1);
+ container.setParent("com.vaadin.ui.TabSheet", "com");
+ p1 = container.getParent("com.vaadin.ui.TabSheet");
+ assertEquals("com", p1);
+ container.setParent("com.vaadin.ui.TabSheet", null);
+ p1 = container.getParent("com.vaadin.ui.TabSheet");
+ assertNull(p1);
+
+ // root -> non-root
+ container.setParent("com.vaadin.ui.TabSheet", "com");
+ p1 = container.getParent("com.vaadin.ui.TabSheet");
+ assertEquals("com", p1);
+
+ }
+
+ @Test
+ public void testHierarchicalFilteringWithParents() {
+ HierarchicalContainer container = new HierarchicalContainer();
+ initializeContainer(container);
+ container.setIncludeParentsWhenFiltering(true);
+
+ // Filter by "contains ab"
+ container.addContainerFilter(FULLY_QUALIFIED_NAME, "ab", false, false);
+
+ // 20 items match the filters and the have 8 parents that should also be
+ // included
+ // only one root "com" should exist
+ // filtered
+ int expectedSize = 29;
+ int expectedRoots = 1;
+
+ validateHierarchicalContainer(container, "com",
+ "com.vaadin.ui.TabSheet",
+ "com.vaadin.terminal.gwt.client.Focusable", "blah", true,
+ expectedSize, expectedRoots, true);
+
+ // only include .gwt.client classes
+ container.removeAllContainerFilters();
+ container.addContainerFilter(FULLY_QUALIFIED_NAME, ".gwt.client.",
+ false, false);
+
+ int packages = 6;
+ int classes = 112;
+
+ expectedSize = packages + classes;
+ expectedRoots = 1;
+
+ validateHierarchicalContainer(container, "com",
+ "com.vaadin.terminal.gwt.client.WidgetSet",
+ "com.vaadin.terminal.gwt.client.ui.VSplitPanelVertical", "blah",
+ true, expectedSize, expectedRoots, true);
+
+ // Additionally remove all without 'm' in the simple name.
+ container.addContainerFilter(SIMPLE_NAME, "m", false, false);
+
+ expectedSize = 7 + 18;
+ expectedRoots = 1;
+
+ validateHierarchicalContainer(container, "com",
+ "com.vaadin.terminal.gwt.client.ui.VUriFragmentUtility",
+ "com.vaadin.terminal.gwt.client.ui.layout.ChildComponentContainer",
+ "blah", true, expectedSize, expectedRoots, true);
+
+ }
+
+ @Test
+ public void testRemoveLastChild() {
+ HierarchicalContainer c = new HierarchicalContainer();
+
+ c.addItem("root");
+ assertEquals(false, c.hasChildren("root"));
+
+ c.addItem("child");
+ c.setParent("child", "root");
+ assertEquals(true, c.hasChildren("root"));
+
+ c.removeItem("child");
+ assertFalse(c.containsId("child"));
+ assertNull(c.getChildren("root"));
+ assertNull(c.getChildren("child"));
+ assertFalse(c.hasChildren("child"));
+ assertFalse(c.hasChildren("root"));
+ }
+
+ @Test
+ public void testRemoveLastChildFromFiltered() {
+ HierarchicalContainer c = new HierarchicalContainer();
+
+ c.addItem("root");
+ assertEquals(false, c.hasChildren("root"));
+
+ c.addItem("child");
+ c.setParent("child", "root");
+ assertEquals(true, c.hasChildren("root"));
+
+ // Dummy filter that does not remove any items
+ c.addContainerFilter(new Filter() {
+
+ @Override
+ public boolean passesFilter(Object itemId, Item item)
+ throws UnsupportedOperationException {
+ return true;
+ }
+
+ @Override
+ public boolean appliesToProperty(Object propertyId) {
+ return true;
+ }
+ });
+ c.removeItem("child");
+
+ assertFalse(c.containsId("child"));
+ assertNull(c.getChildren("root"));
+ assertNull(c.getChildren("child"));
+ assertFalse(c.hasChildren("child"));
+ assertFalse(c.hasChildren("root"));
+ }
+
+ @Test
+ public void testHierarchicalFilteringWithoutParents() {
+ HierarchicalContainer container = new HierarchicalContainer();
+
+ initializeContainer(container);
+ container.setIncludeParentsWhenFiltering(false);
+
+ // Filter by "contains ab"
+ container.addContainerFilter(SIMPLE_NAME, "ab", false, false);
+
+ // 20 items match the filter.
+ // com.vaadin.data.BufferedValidatable
+ // com.vaadin.data.Validatable
+ // com.vaadin.terminal.gwt.client.Focusable
+ // com.vaadin.terminal.gwt.client.Paintable
+ // com.vaadin.terminal.gwt.client.ui.Table
+ // com.vaadin.terminal.gwt.client.ui.VLabel
+ // com.vaadin.terminal.gwt.client.ui.VScrollTable
+ // com.vaadin.terminal.gwt.client.ui.VTablePaging
+ // com.vaadin.terminal.gwt.client.ui.VTabsheet
+ // com.vaadin.terminal.gwt.client.ui.VTabsheetBase
+ // com.vaadin.terminal.gwt.client.ui.VTabsheetPanel
+ // com.vaadin.server.ChangeVariablesErrorEvent
+ // com.vaadin.server.Paintable
+ // com.vaadin.server.Scrollable
+ // com.vaadin.server.Sizeable
+ // com.vaadin.server.VariableOwner
+ // com.vaadin.ui.Label
+ // com.vaadin.ui.Table
+ // com.vaadin.ui.TableFieldFactory
+ // com.vaadin.ui.TabSheet
+ // all become roots.
+ int expectedSize = 20;
+ int expectedRoots = 20;
+
+ validateHierarchicalContainer(container,
+ "com.vaadin.data.BufferedValidatable", "com.vaadin.ui.TabSheet",
+ "com.vaadin.terminal.gwt.client.ui.VTabsheetBase", "blah", true,
+ expectedSize, expectedRoots, false);
+
+ // only include .gwt.client classes
+ container.removeAllContainerFilters();
+ container.addContainerFilter(FULLY_QUALIFIED_NAME, ".gwt.client.",
+ false, false);
+
+ int packages = 3;
+ int classes = 110;
+
+ expectedSize = packages + classes;
+ expectedRoots = 35 + 1; // com.vaadin.terminal.gwt.client.ui +
+ // com.vaadin.terminal.gwt.client.*
+
+ // Sorting is case insensitive
+ validateHierarchicalContainer(container,
+ "com.vaadin.terminal.gwt.client.ApplicationConfiguration",
+ "com.vaadin.terminal.gwt.client.WidgetSet",
+ "com.vaadin.terminal.gwt.client.ui.VOptionGroup", "blah", true,
+ expectedSize, expectedRoots, false);
+
+ // Additionally remove all without 'P' in the simple name.
+ container.addContainerFilter(SIMPLE_NAME, "P", false, false);
+
+ expectedSize = 13;
+ expectedRoots = expectedSize;
+
+ validateHierarchicalContainer(container,
+ "com.vaadin.terminal.gwt.client.Paintable",
+ "com.vaadin.terminal.gwt.client.ui.VTabsheetPanel",
+ "com.vaadin.terminal.gwt.client.ui.VPopupCalendar", "blah",
+ true, expectedSize, expectedRoots, false);
+
+ }
+}
diff --git a/compatibility-server/src/test/java/com/vaadin/data/util/IndexedContainerTest.java b/compatibility-server/src/test/java/com/vaadin/data/util/IndexedContainerTest.java
new file mode 100644
index 0000000000..b19a425518
--- /dev/null
+++ b/compatibility-server/src/test/java/com/vaadin/data/util/IndexedContainerTest.java
@@ -0,0 +1,533 @@
+package com.vaadin.data.util;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertTrue;
+
+import java.util.List;
+
+import org.easymock.Capture;
+import org.easymock.EasyMock;
+import org.junit.Assert;
+import org.junit.Test;
+
+import com.vaadin.data.Container.Indexed.ItemAddEvent;
+import com.vaadin.data.Container.Indexed.ItemRemoveEvent;
+import com.vaadin.data.Container.ItemSetChangeListener;
+import com.vaadin.data.Item;
+
+public class IndexedContainerTest extends AbstractInMemoryContainerTestBase {
+
+ @Test
+ public void testBasicOperations() {
+ testBasicContainerOperations(new IndexedContainer());
+ }
+
+ @Test
+ public void testFiltering() {
+ testContainerFiltering(new IndexedContainer());
+ }
+
+ @Test
+ public void testSorting() {
+ testContainerSorting(new IndexedContainer());
+ }
+
+ @Test
+ public void testSortingAndFiltering() {
+ testContainerSortingAndFiltering(new IndexedContainer());
+ }
+
+ @Test
+ public void testContainerOrdered() {
+ testContainerOrdered(new IndexedContainer());
+ }
+
+ @Test
+ public void testContainerIndexed() {
+ testContainerIndexed(new IndexedContainer(), sampleData[2], 2, true,
+ "newItemId", true);
+ }
+
+ @Test
+ public void testItemSetChangeListeners() {
+ IndexedContainer container = new IndexedContainer();
+ ItemSetChangeCounter counter = new ItemSetChangeCounter();
+ container.addListener(counter);
+
+ String id1 = "id1";
+ String id2 = "id2";
+ String id3 = "id3";
+
+ initializeContainer(container);
+ counter.reset();
+ container.addItem();
+ counter.assertOnce();
+ container.addItem(id1);
+ counter.assertOnce();
+
+ initializeContainer(container);
+ counter.reset();
+ container.addItemAt(0);
+ counter.assertOnce();
+ container.addItemAt(0, id1);
+ counter.assertOnce();
+ container.addItemAt(0, id2);
+ counter.assertOnce();
+ container.addItemAt(container.size(), id3);
+ counter.assertOnce();
+ // no notification if already in container
+ container.addItemAt(0, id1);
+ counter.assertNone();
+
+ initializeContainer(container);
+ counter.reset();
+ container.addItemAfter(null);
+ counter.assertOnce();
+ container.addItemAfter(null, id1);
+ counter.assertOnce();
+ container.addItemAfter(id1);
+ counter.assertOnce();
+ container.addItemAfter(id1, id2);
+ counter.assertOnce();
+ container.addItemAfter(container.firstItemId());
+ counter.assertOnce();
+ container.addItemAfter(container.lastItemId());
+ counter.assertOnce();
+ container.addItemAfter(container.lastItemId(), id3);
+ counter.assertOnce();
+ // no notification if already in container
+ container.addItemAfter(0, id1);
+ counter.assertNone();
+
+ initializeContainer(container);
+ counter.reset();
+ container.removeItem(sampleData[0]);
+ counter.assertOnce();
+
+ initializeContainer(container);
+ counter.reset();
+ // no notification for removing a non-existing item
+ container.removeItem(id1);
+ counter.assertNone();
+
+ initializeContainer(container);
+ counter.reset();
+ container.removeAllItems();
+ counter.assertOnce();
+ // already empty
+ container.removeAllItems();
+ counter.assertNone();
+
+ }
+
+ @Test
+ public void testAddRemoveContainerFilter() {
+ IndexedContainer container = new IndexedContainer();
+ ItemSetChangeCounter counter = new ItemSetChangeCounter();
+ container.addListener(counter);
+
+ // simply adding or removing container filters should cause events
+ // (content changes)
+
+ initializeContainer(container);
+ counter.reset();
+ container.addContainerFilter(SIMPLE_NAME, "a", true, false);
+ counter.assertOnce();
+ container.removeContainerFilters(SIMPLE_NAME);
+ counter.assertOnce();
+ container.addContainerFilter(SIMPLE_NAME, "a", true, false);
+ counter.assertOnce();
+ container.removeAllContainerFilters();
+ counter.assertOnce();
+ }
+
+ // TODO other tests should check positions after removing filter etc,
+ // here concentrating on listeners
+ @Test
+ public void testItemSetChangeListenersFiltering() {
+ IndexedContainer container = new IndexedContainer();
+ ItemSetChangeCounter counter = new ItemSetChangeCounter();
+ container.addListener(counter);
+
+ counter.reset();
+ container.addContainerFilter(FULLY_QUALIFIED_NAME, "Test", true, false);
+ // no real change, so no notification required
+ counter.assertNone();
+
+ String id1 = "com.example.Test1";
+ String id2 = "com.example.Test2";
+ String id3 = "com.example.Other";
+
+ // perform operations while filtering container
+
+ Item item;
+
+ initializeContainer(container);
+ counter.reset();
+ // passes filter
+ item = container.addItem(id1);
+ // no event if filtered out
+ counter.assertNone();
+ item.getItemProperty(FULLY_QUALIFIED_NAME).setValue(id1);
+ counter.assertOnce();
+ // passes filter but already in the container
+ item = container.addItem(id1);
+ counter.assertNone();
+
+ initializeContainer(container);
+ counter.reset();
+ // passes filter after change
+ item = container.addItemAt(0, id1);
+ counter.assertNone();
+ item.getItemProperty(FULLY_QUALIFIED_NAME).setValue(id1);
+ counter.assertOnce();
+ item = container.addItemAt(container.size(), id2);
+ counter.assertNone();
+ item.getItemProperty(FULLY_QUALIFIED_NAME).setValue(id2);
+ counter.assertOnce();
+ // passes filter but already in the container
+ item = container.addItemAt(0, id1);
+ counter.assertNone();
+ item = container.addItemAt(container.size(), id2);
+ counter.assertNone();
+
+ initializeContainer(container);
+ counter.reset();
+ // passes filter
+ item = container.addItemAfter(null, id1);
+ counter.assertNone();
+ item.getItemProperty(FULLY_QUALIFIED_NAME).setValue(id1);
+ counter.assertOnce();
+ item = container.addItemAfter(container.lastItemId(), id2);
+ counter.assertNone();
+ item.getItemProperty(FULLY_QUALIFIED_NAME).setValue(id2);
+ counter.assertOnce();
+ // passes filter but already in the container
+ item = container.addItemAfter(null, id1);
+ counter.assertNone();
+ item = container.addItemAfter(container.lastItemId(), id2);
+ counter.assertNone();
+
+ // does not pass filter
+
+ // TODO implement rest
+
+ initializeContainer(container);
+ counter.reset();
+ item = container.addItemAfter(null, id3);
+ counter.assertNone();
+ item.getItemProperty(FULLY_QUALIFIED_NAME).setValue(id3);
+ counter.assertNone();
+
+ initializeContainer(container);
+ counter.reset();
+ item = container.addItemAfter(container.firstItemId(), id3);
+ counter.assertNone();
+ item.getItemProperty(FULLY_QUALIFIED_NAME).setValue(id3);
+ counter.assertNone();
+
+ initializeContainer(container);
+ counter.reset();
+ item = container.addItemAfter(container.lastItemId(), id3);
+ counter.assertNone();
+ item.getItemProperty(FULLY_QUALIFIED_NAME).setValue(id3);
+ counter.assertNone();
+
+ initializeContainer(container);
+ counter.reset();
+ item = container.addItemAt(0, id3);
+ counter.assertNone();
+ item.getItemProperty(FULLY_QUALIFIED_NAME).setValue(id3);
+ counter.assertNone();
+
+ initializeContainer(container);
+ counter.reset();
+ item = container.addItemAt(1, id3);
+ counter.assertNone();
+ item.getItemProperty(FULLY_QUALIFIED_NAME).setValue(id3);
+ counter.assertNone();
+
+ initializeContainer(container);
+ counter.reset();
+ item = container.addItemAt(container.size(), id3);
+ counter.assertNone();
+ item.getItemProperty(FULLY_QUALIFIED_NAME).setValue(id3);
+ counter.assertNone();
+
+ // passes filter
+
+ initializeContainer(container);
+ counter.reset();
+ item = container.addItem(id1);
+ counter.assertNone();
+ item.getItemProperty(FULLY_QUALIFIED_NAME).setValue(id1);
+ counter.assertOnce();
+ container.removeItem(id1);
+ counter.assertOnce();
+ // already removed
+ container.removeItem(id1);
+ counter.assertNone();
+
+ item = container.addItem(id3);
+ counter.assertNone();
+ item.getItemProperty(FULLY_QUALIFIED_NAME).setValue(id3);
+ counter.assertNone();
+ // not visible
+ container.removeItem(id3);
+ counter.assertNone();
+
+ // remove all
+
+ initializeContainer(container);
+ item = container.addItem(id1);
+ item.getItemProperty(FULLY_QUALIFIED_NAME).setValue(id1);
+ counter.reset();
+ container.removeAllItems();
+ counter.assertOnce();
+ // no visible items
+ container.removeAllItems();
+ counter.assertNone();
+ }
+
+ @Test
+ public void testItemAdd_idSequence() {
+ IndexedContainer container = new IndexedContainer();
+ Object itemId;
+
+ itemId = container.addItem();
+ assertEquals(Integer.valueOf(1), itemId);
+
+ itemId = container.addItem();
+ assertEquals(Integer.valueOf(2), itemId);
+
+ itemId = container.addItemAfter(null);
+ assertEquals(Integer.valueOf(3), itemId);
+
+ itemId = container.addItemAt(2);
+ assertEquals(Integer.valueOf(4), itemId);
+ }
+
+ @Test
+ public void testItemAddRemove_idSequence() {
+ IndexedContainer container = new IndexedContainer();
+ Object itemId;
+
+ itemId = container.addItem();
+ assertEquals(Integer.valueOf(1), itemId);
+
+ container.removeItem(itemId);
+
+ itemId = container.addItem();
+ assertEquals(
+ "Id sequence should continue from the previous value even if an item is removed",
+ Integer.valueOf(2), itemId);
+ }
+
+ @Test
+ public void testItemAddedEvent() {
+ IndexedContainer container = new IndexedContainer();
+ ItemSetChangeListener addListener = createListenerMockFor(container);
+ addListener.containerItemSetChange(EasyMock.isA(ItemAddEvent.class));
+ EasyMock.replay(addListener);
+
+ container.addItem();
+
+ EasyMock.verify(addListener);
+ }
+
+ @Test
+ public void testItemAddedEvent_AddedItem() {
+ IndexedContainer container = new IndexedContainer();
+ ItemSetChangeListener addListener = createListenerMockFor(container);
+ Capture<ItemAddEvent> capturedEvent = captureAddEvent(addListener);
+ EasyMock.replay(addListener);
+
+ Object itemId = container.addItem();
+
+ assertEquals(itemId, capturedEvent.getValue().getFirstItemId());
+ }
+
+ @Test
+ public void testItemAddedEvent_IndexOfAddedItem() {
+ IndexedContainer container = new IndexedContainer();
+ ItemSetChangeListener addListener = createListenerMockFor(container);
+ container.addItem();
+ Capture<ItemAddEvent> capturedEvent = captureAddEvent(addListener);
+ EasyMock.replay(addListener);
+
+ Object itemId = container.addItemAt(1);
+
+ assertEquals(1, capturedEvent.getValue().getFirstIndex());
+ }
+
+ @Test
+ public void testItemRemovedEvent() {
+ IndexedContainer container = new IndexedContainer();
+ Object itemId = container.addItem();
+ ItemSetChangeListener removeListener = createListenerMockFor(container);
+ removeListener
+ .containerItemSetChange(EasyMock.isA(ItemRemoveEvent.class));
+ EasyMock.replay(removeListener);
+
+ container.removeItem(itemId);
+
+ EasyMock.verify(removeListener);
+ }
+
+ @Test
+ public void testItemRemovedEvent_RemovedItem() {
+ IndexedContainer container = new IndexedContainer();
+ Object itemId = container.addItem();
+ ItemSetChangeListener removeListener = createListenerMockFor(container);
+ Capture<ItemRemoveEvent> capturedEvent = captureRemoveEvent(
+ removeListener);
+ EasyMock.replay(removeListener);
+
+ container.removeItem(itemId);
+
+ assertEquals(itemId, capturedEvent.getValue().getFirstItemId());
+ }
+
+ @Test
+ public void testItemRemovedEvent_indexOfRemovedItem() {
+ IndexedContainer container = new IndexedContainer();
+ container.addItem();
+ Object secondItemId = container.addItem();
+ ItemSetChangeListener removeListener = createListenerMockFor(container);
+ Capture<ItemRemoveEvent> capturedEvent = captureRemoveEvent(
+ removeListener);
+ EasyMock.replay(removeListener);
+
+ container.removeItem(secondItemId);
+
+ assertEquals(1, capturedEvent.getValue().getFirstIndex());
+ }
+
+ @Test
+ public void testItemRemovedEvent_amountOfRemovedItems() {
+ IndexedContainer container = new IndexedContainer();
+ container.addItem();
+ container.addItem();
+ ItemSetChangeListener removeListener = createListenerMockFor(container);
+ Capture<ItemRemoveEvent> capturedEvent = captureRemoveEvent(
+ removeListener);
+ EasyMock.replay(removeListener);
+
+ container.removeAllItems();
+
+ assertEquals(2, capturedEvent.getValue().getRemovedItemsCount());
+ }
+
+ private Capture<ItemAddEvent> captureAddEvent(
+ ItemSetChangeListener addListener) {
+ Capture<ItemAddEvent> capturedEvent = new Capture<ItemAddEvent>();
+ addListener.containerItemSetChange(EasyMock.capture(capturedEvent));
+ return capturedEvent;
+ }
+
+ private Capture<ItemRemoveEvent> captureRemoveEvent(
+ ItemSetChangeListener removeListener) {
+ Capture<ItemRemoveEvent> capturedEvent = new Capture<ItemRemoveEvent>();
+ removeListener.containerItemSetChange(EasyMock.capture(capturedEvent));
+ return capturedEvent;
+ }
+
+ private ItemSetChangeListener createListenerMockFor(
+ IndexedContainer container) {
+ ItemSetChangeListener listener = EasyMock
+ .createNiceMock(ItemSetChangeListener.class);
+ container.addItemSetChangeListener(listener);
+ return listener;
+ }
+
+ // Ticket 8028
+ @Test(expected = IndexOutOfBoundsException.class)
+ public void testGetItemIdsRangeIndexOutOfBounds() {
+ IndexedContainer ic = new IndexedContainer();
+ ic.getItemIds(-1, 10);
+ }
+
+ // Ticket 8028
+ @Test(expected = IndexOutOfBoundsException.class)
+ public void testGetItemIdsRangeIndexOutOfBounds2() {
+ IndexedContainer ic = new IndexedContainer();
+ ic.addItem(new Object());
+ ic.getItemIds(2, 1);
+ }
+
+ // Ticket 8028
+ @Test
+ public void testGetItemIdsRangeZeroRange() {
+ IndexedContainer ic = new IndexedContainer();
+ ic.addItem(new Object());
+ List<Object> itemIds = ic.getItemIds(1, 0);
+
+ assertTrue(
+ "Container returned actual values when asking for 0 items...",
+ itemIds.isEmpty());
+ }
+
+ // Ticket 8028
+ @Test(expected = IllegalArgumentException.class)
+ public void testGetItemIdsRangeNegativeRange() {
+ IndexedContainer ic = new IndexedContainer();
+ ic.addItem(new Object());
+ List<Object> itemIds = ic.getItemIds(1, -1);
+
+ assertTrue(
+ "Container returned actual values when asking for -1 items...",
+ itemIds.isEmpty());
+ }
+
+ // Ticket 8028
+ @Test
+ public void testGetItemIdsRangeIndexOutOfBoundsDueToSizeChange() {
+ IndexedContainer ic = new IndexedContainer();
+ ic.addItem(new Object());
+ Assert.assertEquals(
+ "Container returned too many items when the range was >> container size",
+ 1, ic.getItemIds(0, 10).size());
+ }
+
+ // Ticket 8028
+ @Test
+ public void testGetItemIdsRangeBaseCase() {
+ IndexedContainer ic = new IndexedContainer();
+ String object1 = new String("Obj1");
+ String object2 = new String("Obj2");
+ String object3 = new String("Obj3");
+ String object4 = new String("Obj4");
+ String object5 = new String("Obj5");
+
+ ic.addItem(object1);
+ ic.addItem(object2);
+ ic.addItem(object3);
+ ic.addItem(object4);
+ ic.addItem(object5);
+
+ List<Object> itemIds = ic.getItemIds(1, 2);
+
+ assertTrue(itemIds.contains(object2));
+ assertTrue(itemIds.contains(object3));
+ assertEquals(2, itemIds.size());
+ }
+
+ // test getting non-existing property (#10445)
+ @Test
+ public void testNonExistingProperty() {
+ IndexedContainer ic = new IndexedContainer();
+ String object1 = new String("Obj1");
+ ic.addItem(object1);
+ assertNull(ic.getContainerProperty(object1, "xyz"));
+ }
+
+ // test getting null property id (#10445)
+ @Test
+ public void testNullPropertyId() {
+ IndexedContainer ic = new IndexedContainer();
+ String object1 = new String("Obj1");
+ ic.addItem(object1);
+ assertNull(ic.getContainerProperty(object1, null));
+ }
+}
diff --git a/compatibility-server/src/test/java/com/vaadin/data/util/MethodPropertyMemoryConsumptionTest.java b/compatibility-server/src/test/java/com/vaadin/data/util/MethodPropertyMemoryConsumptionTest.java
new file mode 100644
index 0000000000..6776021c24
--- /dev/null
+++ b/compatibility-server/src/test/java/com/vaadin/data/util/MethodPropertyMemoryConsumptionTest.java
@@ -0,0 +1,150 @@
+/*
+ * Copyright 2000-2016 Vaadin Ltd.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not
+ * use this file except in compliance with the License. You may obtain a copy of
+ * the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+package com.vaadin.data.util;
+
+import java.io.ByteArrayInputStream;
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.io.ObjectInputStream;
+import java.io.ObjectOutputStream;
+import java.io.Serializable;
+import java.lang.reflect.Field;
+
+import org.junit.Assert;
+import org.junit.Test;
+
+/**
+ * Test for MethodProperty: don't allocate unnecessary Object arrays.
+ *
+ * @since 7.2
+ * @author Vaadin Ltd
+ */
+public class MethodPropertyMemoryConsumptionTest {
+
+ @Test
+ public void testSetArguments()
+ throws NoSuchFieldException, SecurityException,
+ IllegalArgumentException, IllegalAccessException {
+ TestBean bean = new TestBean();
+ TestMethodProperty<String> property = new TestMethodProperty<String>(
+ bean, "name");
+ Object[] getArgs = property.getGetArgs();
+ Object[] setArgs = property.getSetArgs();
+
+ Field getArgsField = TestMethodProperty.class
+ .getDeclaredField("getArgs");
+ getArgsField.setAccessible(true);
+
+ Field setArgsField = TestMethodProperty.class
+ .getDeclaredField("setArgs");
+ setArgsField.setAccessible(true);
+
+ Assert.assertSame(
+ "setArguments method sets non-default instance"
+ + " of empty Object array for getArgs",
+ getArgsField.get(property), getArgs);
+
+ Assert.assertSame(
+ "setArguments method sets non-default instance"
+ + " of empty Object array for setArgs",
+ setArgsField.get(property), setArgs);
+ }
+
+ @Test
+ public void testDefaultCtor() {
+ TestBean bean = new TestBean();
+ TestMethodProperty<String> property = new TestMethodProperty<String>(
+ bean, "name");
+
+ Object[] getArgs = property.getGetArgs();
+ Object[] setArgs = property.getSetArgs();
+
+ TestBean otherBean = new TestBean();
+ TestMethodProperty<String> otherProperty = new TestMethodProperty<String>(
+ otherBean, "name");
+ Assert.assertSame(
+ "setArguments method uses different instance"
+ + " of empty Object array for getArgs",
+ getArgs, otherProperty.getGetArgs());
+ Assert.assertSame(
+ "setArguments method uses different instance"
+ + " of empty Object array for setArgs",
+ setArgs, otherProperty.getSetArgs());
+ }
+
+ @Test
+ public void testDefaultArgsSerialization()
+ throws IOException, ClassNotFoundException {
+ TestBean bean = new TestBean();
+ TestMethodProperty<String> property = new TestMethodProperty<String>(
+ bean, "name");
+
+ ByteArrayOutputStream sourceOutStream = new ByteArrayOutputStream();
+ ObjectOutputStream outStream = new ObjectOutputStream(sourceOutStream);
+ outStream.writeObject(property);
+
+ ObjectInputStream inputStream = new ObjectInputStream(
+ new ByteArrayInputStream(sourceOutStream.toByteArray()));
+ Object red = inputStream.readObject();
+ TestMethodProperty<?> deserialized = (TestMethodProperty<?>) red;
+
+ Assert.assertNotNull("Deseriliation doesn't call setArguments method",
+ deserialized.getGetArgs());
+ Assert.assertNotNull("Deseriliation doesn't call setArguments method",
+ deserialized.getSetArgs());
+
+ }
+
+ public static class TestMethodProperty<T> extends MethodProperty<T> {
+
+ public TestMethodProperty(Object instance, String beanPropertyName) {
+ super(instance, beanPropertyName);
+ }
+
+ @Override
+ public void setArguments(Object[] getArgs, Object[] setArgs,
+ int setArgumentIndex) {
+ super.setArguments(getArgs, setArgs, setArgumentIndex);
+ this.getArgs = getArgs;
+ this.setArgs = setArgs;
+ }
+
+ Object[] getGetArgs() {
+ return getArgs;
+ }
+
+ Object[] getSetArgs() {
+ return setArgs;
+ }
+
+ private transient Object[] getArgs;
+ private transient Object[] setArgs;
+ }
+
+ public static class TestBean implements Serializable {
+
+ private String name;
+
+ public String getName() {
+ return name;
+ }
+
+ public void setName(String name) {
+ this.name = name;
+ }
+
+ }
+}
diff --git a/compatibility-server/src/test/java/com/vaadin/data/util/NestedMethodPropertyTest.java b/compatibility-server/src/test/java/com/vaadin/data/util/NestedMethodPropertyTest.java
new file mode 100644
index 0000000000..4a1e2a1784
--- /dev/null
+++ b/compatibility-server/src/test/java/com/vaadin/data/util/NestedMethodPropertyTest.java
@@ -0,0 +1,351 @@
+package com.vaadin.data.util;
+
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.fail;
+
+import java.io.ByteArrayInputStream;
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.io.ObjectInputStream;
+import java.io.ObjectOutputStream;
+import java.io.Serializable;
+
+import org.junit.Assert;
+import org.junit.Before;
+import org.junit.Test;
+
+public class NestedMethodPropertyTest {
+
+ public static class Address implements Serializable {
+ private String street;
+ private int postalCodePrimitive;
+ private Integer postalCodeObject;
+
+ public Address(String street, int postalCode) {
+ this.street = street;
+ postalCodePrimitive = postalCode;
+ postalCodeObject = postalCode;
+ }
+
+ public void setStreet(String street) {
+ this.street = street;
+ }
+
+ public String getStreet() {
+ return street;
+ }
+
+ public void setPostalCodePrimitive(int postalCodePrimitive) {
+ this.postalCodePrimitive = postalCodePrimitive;
+ }
+
+ public int getPostalCodePrimitive() {
+ return postalCodePrimitive;
+ }
+
+ public void setPostalCodeObject(Integer postalCodeObject) {
+ this.postalCodeObject = postalCodeObject;
+ }
+
+ public Integer getPostalCodeObject() {
+ return postalCodeObject;
+ }
+
+ // read-only boolean property
+ public boolean isBoolean() {
+ return true;
+ }
+ }
+
+ public static class Person implements Serializable {
+ private String name;
+ private Address address;
+ private int age;
+
+ public Person(String name, Address address) {
+ this.name = name;
+ this.address = address;
+ }
+
+ public Person(String name, Address address, int age) {
+ this.name = name;
+ this.address = address;
+ this.age = age;
+ }
+
+ public void setName(String name) {
+ this.name = name;
+ }
+
+ public String getName() {
+ return name;
+ }
+
+ public void setAddress(Address address) {
+ this.address = address;
+ }
+
+ public Address getAddress() {
+ return address;
+ }
+
+ public int getAge() {
+ return age;
+ }
+
+ public void setAge(int age) {
+ this.age = age;
+ }
+ }
+
+ public static class Team implements Serializable {
+ private String name;
+ private Person manager;
+
+ public Team(String name, Person manager) {
+ this.name = name;
+ this.manager = manager;
+ }
+
+ public void setName(String name) {
+ this.name = name;
+ }
+
+ public String getName() {
+ return name;
+ }
+
+ public void setManager(Person manager) {
+ this.manager = manager;
+ }
+
+ public Person getManager() {
+ return manager;
+ }
+ }
+
+ private Address oldMill;
+ private Person joonas;
+ private Team vaadin;
+
+ @Before
+ public void setUp() {
+ oldMill = new Address("Ruukinkatu 2-4", 20540);
+ joonas = new Person("Joonas", oldMill);
+ vaadin = new Team("Vaadin", joonas);
+ }
+
+ @Test
+ public void testSingleLevelNestedSimpleProperty() {
+ NestedMethodProperty<String> nameProperty = new NestedMethodProperty<String>(
+ vaadin, "name");
+
+ Assert.assertEquals(String.class, nameProperty.getType());
+ Assert.assertEquals("Vaadin", nameProperty.getValue());
+ }
+
+ @Test
+ public void testSingleLevelNestedObjectProperty() {
+ NestedMethodProperty<Person> managerProperty = new NestedMethodProperty<Person>(
+ vaadin, "manager");
+
+ Assert.assertEquals(Person.class, managerProperty.getType());
+ Assert.assertEquals(joonas, managerProperty.getValue());
+ }
+
+ @Test
+ public void testMultiLevelNestedProperty() {
+ NestedMethodProperty<String> managerNameProperty = new NestedMethodProperty<String>(
+ vaadin, "manager.name");
+ NestedMethodProperty<Address> addressProperty = new NestedMethodProperty<Address>(
+ vaadin, "manager.address");
+ NestedMethodProperty<String> streetProperty = new NestedMethodProperty<String>(
+ vaadin, "manager.address.street");
+ NestedMethodProperty<Integer> postalCodePrimitiveProperty = new NestedMethodProperty<Integer>(
+ vaadin, "manager.address.postalCodePrimitive");
+ NestedMethodProperty<Integer> postalCodeObjectProperty = new NestedMethodProperty<Integer>(
+ vaadin, "manager.address.postalCodeObject");
+ NestedMethodProperty<Boolean> booleanProperty = new NestedMethodProperty<Boolean>(
+ vaadin, "manager.address.boolean");
+
+ Assert.assertEquals(String.class, managerNameProperty.getType());
+ Assert.assertEquals("Joonas", managerNameProperty.getValue());
+
+ Assert.assertEquals(Address.class, addressProperty.getType());
+ Assert.assertEquals(oldMill, addressProperty.getValue());
+
+ Assert.assertEquals(String.class, streetProperty.getType());
+ Assert.assertEquals("Ruukinkatu 2-4", streetProperty.getValue());
+
+ Assert.assertEquals(Integer.class,
+ postalCodePrimitiveProperty.getType());
+ Assert.assertEquals(Integer.valueOf(20540),
+ postalCodePrimitiveProperty.getValue());
+
+ Assert.assertEquals(Integer.class, postalCodeObjectProperty.getType());
+ Assert.assertEquals(Integer.valueOf(20540),
+ postalCodeObjectProperty.getValue());
+
+ Assert.assertEquals(Boolean.class, booleanProperty.getType());
+ Assert.assertEquals(Boolean.TRUE, booleanProperty.getValue());
+ }
+
+ @Test
+ public void testEmptyPropertyName() {
+ try {
+ new NestedMethodProperty<Object>(vaadin, "");
+ fail();
+ } catch (IllegalArgumentException e) {
+ // should get exception
+ }
+
+ try {
+ new NestedMethodProperty<Object>(vaadin, " ");
+ fail();
+ } catch (IllegalArgumentException e) {
+ // should get exception
+ }
+ }
+
+ @Test
+ public void testInvalidPropertyName() {
+ try {
+ new NestedMethodProperty<Object>(vaadin, ".");
+ fail();
+ } catch (IllegalArgumentException e) {
+ // should get exception
+ }
+ try {
+ new NestedMethodProperty<Object>(vaadin, ".manager");
+ fail();
+ } catch (IllegalArgumentException e) {
+ // should get exception
+ }
+ try {
+ new NestedMethodProperty<Object>(vaadin, "manager.");
+ fail();
+ } catch (IllegalArgumentException e) {
+ // should get exception
+ }
+ try {
+ new NestedMethodProperty<Object>(vaadin, "manager..name");
+ fail();
+ } catch (IllegalArgumentException e) {
+ // should get exception
+ }
+ }
+
+ @Test
+ public void testInvalidNestedPropertyName() {
+ try {
+ new NestedMethodProperty<Object>(vaadin, "member");
+ fail();
+ } catch (IllegalArgumentException e) {
+ // should get exception
+ }
+
+ try {
+ new NestedMethodProperty<Object>(vaadin, "manager.pet");
+ fail();
+ } catch (IllegalArgumentException e) {
+ // should get exception
+ }
+
+ try {
+ new NestedMethodProperty<Object>(vaadin, "manager.address.city");
+ fail();
+ } catch (IllegalArgumentException e) {
+ // should get exception
+ }
+ }
+
+ @Test
+ public void testNullNestedProperty() {
+ NestedMethodProperty<String> managerNameProperty = new NestedMethodProperty<String>(
+ vaadin, "manager.name");
+ NestedMethodProperty<String> streetProperty = new NestedMethodProperty<String>(
+ vaadin, "manager.address.street");
+
+ joonas.setAddress(null);
+ assertNull(streetProperty.getValue());
+
+ vaadin.setManager(null);
+ assertNull(managerNameProperty.getValue());
+ assertNull(streetProperty.getValue());
+
+ vaadin.setManager(joonas);
+ Assert.assertEquals("Joonas", managerNameProperty.getValue());
+ Assert.assertNull(streetProperty.getValue());
+
+ }
+
+ @Test
+ public void testMultiLevelNestedPropertySetValue() {
+ NestedMethodProperty<String> managerNameProperty = new NestedMethodProperty<String>(
+ vaadin, "manager.name");
+ NestedMethodProperty<Address> addressProperty = new NestedMethodProperty<Address>(
+ vaadin, "manager.address");
+ NestedMethodProperty<String> streetProperty = new NestedMethodProperty<String>(
+ vaadin, "manager.address.street");
+ NestedMethodProperty<Integer> postalCodePrimitiveProperty = new NestedMethodProperty<Integer>(
+ vaadin, "manager.address.postalCodePrimitive");
+ NestedMethodProperty<Integer> postalCodeObjectProperty = new NestedMethodProperty<Integer>(
+ vaadin, "manager.address.postalCodeObject");
+
+ managerNameProperty.setValue("Joonas L");
+ Assert.assertEquals("Joonas L", joonas.getName());
+ streetProperty.setValue("Ruukinkatu");
+ Assert.assertEquals("Ruukinkatu", oldMill.getStreet());
+ postalCodePrimitiveProperty.setValue(0);
+ postalCodeObjectProperty.setValue(1);
+ Assert.assertEquals(0, oldMill.getPostalCodePrimitive());
+ Assert.assertEquals(Integer.valueOf(1), oldMill.getPostalCodeObject());
+
+ postalCodeObjectProperty.setValue(null);
+ Assert.assertNull(oldMill.getPostalCodeObject());
+
+ Address address2 = new Address("Other street", 12345);
+ addressProperty.setValue(address2);
+ Assert.assertEquals("Other street", streetProperty.getValue());
+ }
+
+ @Test
+ public void testSerialization() throws IOException, ClassNotFoundException {
+ NestedMethodProperty<String> streetProperty = new NestedMethodProperty<String>(
+ vaadin, "manager.address.street");
+ ByteArrayOutputStream baos = new ByteArrayOutputStream();
+ new ObjectOutputStream(baos).writeObject(streetProperty);
+ @SuppressWarnings("unchecked")
+ NestedMethodProperty<String> property2 = (NestedMethodProperty<String>) new ObjectInputStream(
+ new ByteArrayInputStream(baos.toByteArray())).readObject();
+
+ Assert.assertEquals("Ruukinkatu 2-4", property2.getValue());
+ }
+
+ @Test
+ public void testSerializationWithIntermediateNull()
+ throws IOException, ClassNotFoundException {
+ vaadin.setManager(null);
+ NestedMethodProperty<String> streetProperty = new NestedMethodProperty<String>(
+ vaadin, "manager.address.street");
+ ByteArrayOutputStream baos = new ByteArrayOutputStream();
+ new ObjectOutputStream(baos).writeObject(streetProperty);
+ @SuppressWarnings("unchecked")
+ NestedMethodProperty<String> property2 = (NestedMethodProperty<String>) new ObjectInputStream(
+ new ByteArrayInputStream(baos.toByteArray())).readObject();
+
+ Assert.assertNull(property2.getValue());
+ }
+
+ @Test
+ public void testIsReadOnly() {
+ NestedMethodProperty<String> streetProperty = new NestedMethodProperty<String>(
+ vaadin, "manager.address.street");
+ NestedMethodProperty<Boolean> booleanProperty = new NestedMethodProperty<Boolean>(
+ vaadin, "manager.address.boolean");
+
+ Assert.assertFalse(streetProperty.isReadOnly());
+ Assert.assertTrue(booleanProperty.isReadOnly());
+ }
+
+}
diff --git a/compatibility-server/src/test/java/com/vaadin/data/util/ObjectPropertyTest.java b/compatibility-server/src/test/java/com/vaadin/data/util/ObjectPropertyTest.java
new file mode 100644
index 0000000000..e0307034cf
--- /dev/null
+++ b/compatibility-server/src/test/java/com/vaadin/data/util/ObjectPropertyTest.java
@@ -0,0 +1,102 @@
+package com.vaadin.data.util;
+
+import org.junit.Assert;
+import org.junit.Test;
+
+public class ObjectPropertyTest {
+
+ public static class TestSuperClass {
+ private String name;
+
+ public TestSuperClass(String name) {
+ this.name = name;
+ }
+
+ public String getName() {
+ return name;
+ }
+
+ @Override
+ public String toString() {
+ return getName();
+ }
+ }
+
+ public static class TestSubClass extends TestSuperClass {
+ public TestSubClass(String name) {
+ super("Subclass: " + name);
+ }
+ }
+
+ private TestSuperClass super1 = new TestSuperClass("super1");
+ private TestSubClass sub1 = new TestSubClass("sub1");
+
+ @Test
+ public void testSimple() {
+ ObjectProperty<TestSuperClass> prop1 = new ObjectProperty<TestSuperClass>(
+ super1, TestSuperClass.class);
+ Assert.assertEquals("super1", prop1.getValue().getName());
+ prop1 = new ObjectProperty<TestSuperClass>(super1);
+ Assert.assertEquals("super1", prop1.getValue().getName());
+
+ ObjectProperty<TestSubClass> prop2 = new ObjectProperty<TestSubClass>(
+ sub1, TestSubClass.class);
+ Assert.assertEquals("Subclass: sub1", prop2.getValue().getName());
+ prop2 = new ObjectProperty<TestSubClass>(sub1);
+ Assert.assertEquals("Subclass: sub1", prop2.getValue().getName());
+ }
+
+ @Test
+ public void testSetValueObjectSuper() {
+ ObjectProperty<TestSuperClass> prop = new ObjectProperty<TestSuperClass>(
+ super1, TestSuperClass.class);
+ Assert.assertEquals("super1", prop.getValue().getName());
+ prop.setValue(new TestSuperClass("super2"));
+ Assert.assertEquals("super1", super1.getName());
+ Assert.assertEquals("super2", prop.getValue().getName());
+ }
+
+ @Test
+ public void testSetValueObjectSub() {
+ ObjectProperty<TestSubClass> prop = new ObjectProperty<TestSubClass>(
+ sub1, TestSubClass.class);
+ Assert.assertEquals("Subclass: sub1", prop.getValue().getName());
+ prop.setValue(new TestSubClass("sub2"));
+ Assert.assertEquals("Subclass: sub1", sub1.getName());
+ Assert.assertEquals("Subclass: sub2", prop.getValue().getName());
+ }
+
+ @Test
+ public void testSetValueStringSuper() {
+ ObjectProperty<TestSuperClass> prop = new ObjectProperty<TestSuperClass>(
+ super1, TestSuperClass.class);
+ Assert.assertEquals("super1", prop.getValue().getName());
+ prop.setValue(new TestSuperClass("super2"));
+ Assert.assertEquals("super1", super1.getName());
+ Assert.assertEquals("super2", prop.getValue().getName());
+ }
+
+ @Test
+ public void testSetValueStringSub() {
+ ObjectProperty<TestSubClass> prop = new ObjectProperty<TestSubClass>(
+ sub1, TestSubClass.class);
+ Assert.assertEquals("Subclass: sub1", prop.getValue().getName());
+ prop.setValue(new TestSubClass("sub2"));
+ Assert.assertEquals("Subclass: sub1", sub1.getName());
+ Assert.assertEquals("Subclass: sub2", prop.getValue().getName());
+ }
+
+ @Test
+ public void testMixedGenerics() {
+ ObjectProperty<TestSuperClass> prop = new ObjectProperty<TestSuperClass>(
+ sub1);
+ Assert.assertEquals("Subclass: sub1", prop.getValue().getName());
+ Assert.assertEquals(prop.getType(), TestSubClass.class);
+ // create correct subclass based on the runtime type of the instance
+ // given to ObjectProperty constructor, which is a subclass of the type
+ // parameter
+ prop.setValue(new TestSubClass("sub2"));
+ Assert.assertEquals("Subclass: sub2", prop.getValue().getName());
+ }
+
+}
diff --git a/compatibility-server/src/test/java/com/vaadin/data/util/PerformanceTestIndexedContainerTest.java b/compatibility-server/src/test/java/com/vaadin/data/util/PerformanceTestIndexedContainerTest.java
new file mode 100644
index 0000000000..5f64c0e8d8
--- /dev/null
+++ b/compatibility-server/src/test/java/com/vaadin/data/util/PerformanceTestIndexedContainerTest.java
@@ -0,0 +1,120 @@
+package com.vaadin.data.util;
+
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.SortedSet;
+import java.util.TreeSet;
+
+import org.junit.Assert;
+import org.junit.Test;
+
+public class PerformanceTestIndexedContainerTest {
+
+ private static final int REPEATS = 10;
+ private final static int ITEMS = 50000;
+ private static final long ADD_ITEM_FAIL_THRESHOLD = 200;
+ // TODO should improve performance of these methods
+ private static final long ADD_ITEM_AT_FAIL_THRESHOLD = 5000;
+ private static final long ADD_ITEM_AFTER_FAIL_THRESHOLD = 5000;
+ private static final long ADD_ITEM_AFTER_LAST_FAIL_THRESHOLD = 5000;
+ private static final long ADD_ITEMS_CONSTRUCTOR_FAIL_THRESHOLD = 200;
+
+ @Test
+ public void testAddItemPerformance() {
+ Collection<Long> times = new ArrayList<Long>();
+ for (int j = 0; j < REPEATS; ++j) {
+ IndexedContainer c = new IndexedContainer();
+ long start = System.currentTimeMillis();
+ for (int i = 0; i < ITEMS; i++) {
+ c.addItem();
+ }
+ times.add(System.currentTimeMillis() - start);
+ }
+ checkMedian(ITEMS, times, "IndexedContainer.addItem()",
+ ADD_ITEM_FAIL_THRESHOLD);
+ }
+
+ @Test
+ public void testAddItemAtPerformance() {
+ Collection<Long> times = new ArrayList<Long>();
+ for (int j = 0; j < REPEATS; ++j) {
+ IndexedContainer c = new IndexedContainer();
+ long start = System.currentTimeMillis();
+ for (int i = 0; i < ITEMS; i++) {
+ c.addItemAt(0);
+ }
+ times.add(System.currentTimeMillis() - start);
+ }
+ checkMedian(ITEMS, times, "IndexedContainer.addItemAt()",
+ ADD_ITEM_AT_FAIL_THRESHOLD);
+ }
+
+ @Test
+ public void testAddItemAfterPerformance() {
+ Object initialId = "Item0";
+ Collection<Long> times = new ArrayList<Long>();
+ for (int j = 0; j < REPEATS; ++j) {
+ IndexedContainer c = new IndexedContainer();
+ c.addItem(initialId);
+ long start = System.currentTimeMillis();
+ for (int i = 0; i < ITEMS; i++) {
+ c.addItemAfter(initialId);
+ }
+ times.add(System.currentTimeMillis() - start);
+ }
+ checkMedian(ITEMS, times, "IndexedContainer.addItemAfter()",
+ ADD_ITEM_AFTER_FAIL_THRESHOLD);
+ }
+
+ @Test
+ public void testAddItemAfterLastPerformance() {
+ // TODO running with less items because slow otherwise
+ Collection<Long> times = new ArrayList<Long>();
+ for (int j = 0; j < REPEATS; ++j) {
+ IndexedContainer c = new IndexedContainer();
+ c.addItem();
+ long start = System.currentTimeMillis();
+ for (int i = 0; i < ITEMS / 3; i++) {
+ c.addItemAfter(c.lastItemId());
+ }
+ times.add(System.currentTimeMillis() - start);
+ }
+ checkMedian(ITEMS / 3, times, "IndexedContainer.addItemAfter(lastId)",
+ ADD_ITEM_AFTER_LAST_FAIL_THRESHOLD);
+ }
+
+ @Test
+ public void testAddItemsConstructorPerformance() {
+ Collection<Object> items = new ArrayList<Object>(50000);
+ for (int i = 0; i < ITEMS; ++i) {
+ items.add(new Object());
+ }
+
+ SortedSet<Long> times = new TreeSet<Long>();
+ for (int j = 0; j < REPEATS; ++j) {
+ long start = System.currentTimeMillis();
+ new IndexedContainer(items);
+ times.add(System.currentTimeMillis() - start);
+ }
+ checkMedian(ITEMS, times, "IndexedContainer(Collection)",
+ ADD_ITEMS_CONSTRUCTOR_FAIL_THRESHOLD);
+ }
+
+ private void checkMedian(int items, Collection<Long> times,
+ String methodName, long threshold) {
+ long median = median(times);
+ System.out.println(
+ methodName + " timings (ms) for " + items + " items: " + times);
+ Assert.assertTrue(methodName + " too slow, median time " + median
+ + "ms for " + items + " items", median <= threshold);
+ }
+
+ private Long median(Collection<Long> times) {
+ ArrayList<Long> list = new ArrayList<Long>(times);
+ Collections.sort(list);
+ // not exact median in some cases, but good enough
+ return list.get(list.size() / 2);
+ }
+
+}
diff --git a/compatibility-server/src/test/java/com/vaadin/data/util/PropertyDescriptorTest.java b/compatibility-server/src/test/java/com/vaadin/data/util/PropertyDescriptorTest.java
new file mode 100644
index 0000000000..af9db229c5
--- /dev/null
+++ b/compatibility-server/src/test/java/com/vaadin/data/util/PropertyDescriptorTest.java
@@ -0,0 +1,84 @@
+package com.vaadin.data.util;
+
+import java.beans.Introspector;
+import java.beans.PropertyDescriptor;
+import java.io.ByteArrayInputStream;
+import java.io.ByteArrayOutputStream;
+import java.io.ObjectInputStream;
+import java.io.ObjectOutputStream;
+
+import org.junit.Assert;
+import org.junit.Test;
+
+import com.vaadin.data.Property;
+import com.vaadin.data.util.NestedMethodPropertyTest.Person;
+
+public class PropertyDescriptorTest {
+
+ @Test
+ public void testMethodPropertyDescriptorSerialization() throws Exception {
+ PropertyDescriptor[] pds = Introspector.getBeanInfo(Person.class)
+ .getPropertyDescriptors();
+
+ MethodPropertyDescriptor<Person> descriptor = null;
+
+ for (PropertyDescriptor pd : pds) {
+ if ("name".equals(pd.getName())) {
+ descriptor = new MethodPropertyDescriptor<Person>(pd.getName(),
+ String.class, pd.getReadMethod(), pd.getWriteMethod());
+ break;
+ }
+ }
+
+ ByteArrayOutputStream baos = new ByteArrayOutputStream();
+ new ObjectOutputStream(baos).writeObject(descriptor);
+ @SuppressWarnings("unchecked")
+ VaadinPropertyDescriptor<Person> descriptor2 = (VaadinPropertyDescriptor<Person>) new ObjectInputStream(
+ new ByteArrayInputStream(baos.toByteArray())).readObject();
+
+ Property<?> property = descriptor2
+ .createProperty(new Person("John", null));
+ Assert.assertEquals("John", property.getValue());
+ }
+
+ @Test
+ public void testSimpleNestedPropertyDescriptorSerialization()
+ throws Exception {
+ NestedPropertyDescriptor<Person> pd = new NestedPropertyDescriptor<Person>(
+ "name", Person.class);
+
+ ByteArrayOutputStream baos = new ByteArrayOutputStream();
+ new ObjectOutputStream(baos).writeObject(pd);
+ @SuppressWarnings("unchecked")
+ VaadinPropertyDescriptor<Person> pd2 = (VaadinPropertyDescriptor<Person>) new ObjectInputStream(
+ new ByteArrayInputStream(baos.toByteArray())).readObject();
+
+ Property<?> property = pd2.createProperty(new Person("John", null));
+ Assert.assertEquals("John", property.getValue());
+ }
+
+ @Test
+ public void testNestedPropertyDescriptorSerialization() throws Exception {
+ NestedPropertyDescriptor<Person> pd = new NestedPropertyDescriptor<Person>(
+ "address.street", Person.class);
+
+ ByteArrayOutputStream baos = new ByteArrayOutputStream();
+ new ObjectOutputStream(baos).writeObject(pd);
+ @SuppressWarnings("unchecked")
+ VaadinPropertyDescriptor<Person> pd2 = (VaadinPropertyDescriptor<Person>) new ObjectInputStream(
+ new ByteArrayInputStream(baos.toByteArray())).readObject();
+
+ Property<?> property = pd2.createProperty(new Person("John", null));
+ Assert.assertNull(property.getValue());
+ }
+
+ @Test
+ public void testMethodPropertyDescriptorWithPrimitivePropertyType()
+ throws Exception {
+ MethodPropertyDescriptor<Person> pd = new MethodPropertyDescriptor<Person>(
+ "age", int.class, Person.class.getMethod("getAge"),
+ Person.class.getMethod("setAge", int.class));
+
+ Assert.assertEquals(Integer.class, pd.getPropertyType());
+ }
+}
diff --git a/compatibility-server/src/test/java/com/vaadin/data/util/PropertySetItemTest.java b/compatibility-server/src/test/java/com/vaadin/data/util/PropertySetItemTest.java
new file mode 100644
index 0000000000..fc91a20dd0
--- /dev/null
+++ b/compatibility-server/src/test/java/com/vaadin/data/util/PropertySetItemTest.java
@@ -0,0 +1,432 @@
+package com.vaadin.data.util;
+
+import java.util.Iterator;
+
+import org.easymock.EasyMock;
+import org.junit.After;
+import org.junit.Assert;
+import org.junit.Before;
+import org.junit.Test;
+
+import com.vaadin.data.Item.PropertySetChangeEvent;
+import com.vaadin.data.Item.PropertySetChangeListener;
+
+public class PropertySetItemTest {
+
+ private static final String ID1 = "id1";
+ private static final String ID2 = "id2";
+ private static final String ID3 = "id3";
+
+ private static final String VALUE1 = "value1";
+ private static final String VALUE2 = "value2";
+ private static final String VALUE3 = "value3";
+
+ private ObjectProperty<String> prop1;
+ private ObjectProperty<String> prop2;
+ private ObjectProperty<String> prop3;
+
+ private PropertySetChangeListener propertySetListenerMock;
+ private PropertySetChangeListener propertySetListenerMock2;
+
+ @Before
+ public void setUp() {
+ prop1 = new ObjectProperty<String>(VALUE1, String.class);
+ prop2 = new ObjectProperty<String>(VALUE2, String.class);
+ prop3 = new ObjectProperty<String>(VALUE3, String.class);
+
+ propertySetListenerMock = EasyMock
+ .createStrictMock(PropertySetChangeListener.class);
+ propertySetListenerMock2 = EasyMock
+ .createMock(PropertySetChangeListener.class);
+ }
+
+ @After
+ public void tearDown() {
+ prop1 = null;
+ prop2 = null;
+ prop3 = null;
+
+ propertySetListenerMock = null;
+ propertySetListenerMock2 = null;
+ }
+
+ private PropertysetItem createPropertySetItem() {
+ return new PropertysetItem();
+ }
+
+ @Test
+ public void testEmptyItem() {
+ PropertysetItem item = createPropertySetItem();
+ Assert.assertNotNull(item.getItemPropertyIds());
+ Assert.assertEquals(0, item.getItemPropertyIds().size());
+ }
+
+ @Test
+ public void testGetProperty() {
+ PropertysetItem item = createPropertySetItem();
+
+ Assert.assertNull(item.getItemProperty(ID1));
+
+ item.addItemProperty(ID1, prop1);
+
+ Assert.assertEquals(prop1, item.getItemProperty(ID1));
+ Assert.assertNull(item.getItemProperty(ID2));
+ }
+
+ @Test
+ public void testAddSingleProperty() {
+ PropertysetItem item = createPropertySetItem();
+
+ item.addItemProperty(ID1, prop1);
+ Assert.assertEquals(1, item.getItemPropertyIds().size());
+ Object firstValue = item.getItemPropertyIds().iterator().next();
+ Assert.assertEquals(ID1, firstValue);
+ Assert.assertEquals(prop1, item.getItemProperty(ID1));
+ }
+
+ @Test
+ public void testAddMultipleProperties() {
+ PropertysetItem item = createPropertySetItem();
+
+ item.addItemProperty(ID1, prop1);
+ Assert.assertEquals(1, item.getItemPropertyIds().size());
+ Assert.assertEquals(prop1, item.getItemProperty(ID1));
+
+ item.addItemProperty(ID2, prop2);
+ Assert.assertEquals(2, item.getItemPropertyIds().size());
+ Assert.assertEquals(prop1, item.getItemProperty(ID1));
+ Assert.assertEquals(prop2, item.getItemProperty(ID2));
+
+ item.addItemProperty(ID3, prop3);
+ Assert.assertEquals(3, item.getItemPropertyIds().size());
+ }
+
+ @Test
+ public void testAddedPropertyOrder() {
+ PropertysetItem item = createPropertySetItem();
+ item.addItemProperty(ID1, prop1);
+ item.addItemProperty(ID2, prop2);
+ item.addItemProperty(ID3, prop3);
+
+ Iterator<?> it = item.getItemPropertyIds().iterator();
+ Assert.assertEquals(ID1, it.next());
+ Assert.assertEquals(ID2, it.next());
+ Assert.assertEquals(ID3, it.next());
+ }
+
+ @Test
+ public void testAddPropertyTwice() {
+ PropertysetItem item = createPropertySetItem();
+ Assert.assertTrue(item.addItemProperty(ID1, prop1));
+ Assert.assertFalse(item.addItemProperty(ID1, prop1));
+
+ Assert.assertEquals(1, item.getItemPropertyIds().size());
+ Assert.assertEquals(prop1, item.getItemProperty(ID1));
+ }
+
+ @Test
+ public void testCannotChangeProperty() {
+ PropertysetItem item = createPropertySetItem();
+ Assert.assertTrue(item.addItemProperty(ID1, prop1));
+
+ Assert.assertEquals(prop1, item.getItemProperty(ID1));
+
+ Assert.assertFalse(item.addItemProperty(ID1, prop2));
+
+ Assert.assertEquals(1, item.getItemPropertyIds().size());
+ Assert.assertEquals(prop1, item.getItemProperty(ID1));
+ }
+
+ @Test
+ public void testRemoveProperty() {
+ PropertysetItem item = createPropertySetItem();
+ item.addItemProperty(ID1, prop1);
+ item.removeItemProperty(ID1);
+
+ Assert.assertEquals(0, item.getItemPropertyIds().size());
+ Assert.assertNull(item.getItemProperty(ID1));
+ }
+
+ @Test
+ public void testRemovePropertyOrder() {
+ PropertysetItem item = createPropertySetItem();
+ item.addItemProperty(ID1, prop1);
+ item.addItemProperty(ID2, prop2);
+ item.addItemProperty(ID3, prop3);
+
+ item.removeItemProperty(ID2);
+
+ Iterator<?> it = item.getItemPropertyIds().iterator();
+ Assert.assertEquals(ID1, it.next());
+ Assert.assertEquals(ID3, it.next());
+ }
+
+ @Test
+ public void testRemoveNonExistentListener() {
+ PropertysetItem item = createPropertySetItem();
+ item.removeListener(propertySetListenerMock);
+ }
+
+ @Test
+ public void testRemoveListenerTwice() {
+ PropertysetItem item = createPropertySetItem();
+ item.addListener(propertySetListenerMock);
+ item.removeListener(propertySetListenerMock);
+ item.removeListener(propertySetListenerMock);
+ }
+
+ @Test
+ public void testAddPropertyNotification() {
+ // exactly one notification each time
+ PropertysetItem item = createPropertySetItem();
+
+ // Expectations and start test
+ propertySetListenerMock.itemPropertySetChange(
+ EasyMock.isA(PropertySetChangeEvent.class));
+ EasyMock.replay(propertySetListenerMock);
+
+ // Add listener and add a property -> should end up in listener once
+ item.addListener(propertySetListenerMock);
+ item.addItemProperty(ID1, prop1);
+
+ // Ensure listener was called once
+ EasyMock.verify(propertySetListenerMock);
+
+ // Remove the listener -> should not end up in listener when adding a
+ // property
+ item.removeListener(propertySetListenerMock);
+ item.addItemProperty(ID2, prop2);
+
+ // Ensure listener still has been called only once
+ EasyMock.verify(propertySetListenerMock);
+ }
+
+ @Test
+ public void testRemovePropertyNotification() {
+ // exactly one notification each time
+ PropertysetItem item = createPropertySetItem();
+ item.addItemProperty(ID1, prop1);
+ item.addItemProperty(ID2, prop2);
+
+ // Expectations and start test
+ propertySetListenerMock.itemPropertySetChange(
+ EasyMock.isA(PropertySetChangeEvent.class));
+ EasyMock.replay(propertySetListenerMock);
+
+ // Add listener and add a property -> should end up in listener once
+ item.addListener(propertySetListenerMock);
+ item.removeItemProperty(ID1);
+
+ // Ensure listener was called once
+ EasyMock.verify(propertySetListenerMock);
+
+ // Remove the listener -> should not end up in listener
+ item.removeListener(propertySetListenerMock);
+ item.removeItemProperty(ID2);
+
+ // Ensure listener still has been called only once
+ EasyMock.verify(propertySetListenerMock);
+ }
+
+ @Test
+ public void testItemEqualsNull() {
+ PropertysetItem item = createPropertySetItem();
+
+ Assert.assertFalse(item.equals(null));
+ }
+
+ @Test
+ public void testEmptyItemEquals() {
+ PropertysetItem item1 = createPropertySetItem();
+ PropertysetItem item2 = createPropertySetItem();
+
+ Assert.assertTrue(item1.equals(item2));
+ }
+
+ @Test
+ public void testItemEqualsSingleProperty() {
+ PropertysetItem item1 = createPropertySetItem();
+ PropertysetItem item2 = createPropertySetItem();
+ item2.addItemProperty(ID1, prop1);
+ PropertysetItem item3 = createPropertySetItem();
+ item3.addItemProperty(ID1, prop1);
+ PropertysetItem item4 = createPropertySetItem();
+ item4.addItemProperty(ID1, prop2);
+ PropertysetItem item5 = createPropertySetItem();
+ item5.addItemProperty(ID2, prop2);
+
+ Assert.assertFalse(item1.equals(item2));
+ Assert.assertFalse(item1.equals(item3));
+ Assert.assertFalse(item1.equals(item4));
+ Assert.assertFalse(item1.equals(item5));
+
+ Assert.assertTrue(item2.equals(item3));
+ Assert.assertFalse(item2.equals(item4));
+ Assert.assertFalse(item2.equals(item5));
+
+ Assert.assertFalse(item3.equals(item4));
+ Assert.assertFalse(item3.equals(item5));
+
+ Assert.assertFalse(item4.equals(item5));
+
+ Assert.assertFalse(item2.equals(item1));
+ }
+
+ @Test
+ public void testItemEqualsMultipleProperties() {
+ PropertysetItem item1 = createPropertySetItem();
+ item1.addItemProperty(ID1, prop1);
+
+ PropertysetItem item2 = createPropertySetItem();
+ item2.addItemProperty(ID1, prop1);
+ item2.addItemProperty(ID2, prop2);
+
+ PropertysetItem item3 = createPropertySetItem();
+ item3.addItemProperty(ID1, prop1);
+ item3.addItemProperty(ID2, prop2);
+
+ Assert.assertFalse(item1.equals(item2));
+
+ Assert.assertTrue(item2.equals(item3));
+ }
+
+ @Test
+ public void testItemEqualsPropertyOrder() {
+ PropertysetItem item1 = createPropertySetItem();
+ item1.addItemProperty(ID1, prop1);
+ item1.addItemProperty(ID2, prop2);
+
+ PropertysetItem item2 = createPropertySetItem();
+ item2.addItemProperty(ID2, prop2);
+ item2.addItemProperty(ID1, prop1);
+
+ Assert.assertFalse(item1.equals(item2));
+ }
+
+ @Test
+ public void testEqualsSingleListener() {
+ PropertysetItem item1 = createPropertySetItem();
+ PropertysetItem item2 = createPropertySetItem();
+
+ item1.addListener(propertySetListenerMock);
+
+ Assert.assertFalse(item1.equals(item2));
+ Assert.assertFalse(item2.equals(item1));
+
+ item2.addListener(propertySetListenerMock);
+
+ Assert.assertTrue(item1.equals(item2));
+ Assert.assertTrue(item2.equals(item1));
+ }
+
+ @Test
+ public void testEqualsMultipleListeners() {
+ PropertysetItem item1 = createPropertySetItem();
+ PropertysetItem item2 = createPropertySetItem();
+
+ item1.addListener(propertySetListenerMock);
+ item1.addListener(propertySetListenerMock2);
+
+ item2.addListener(propertySetListenerMock);
+
+ Assert.assertFalse(item1.equals(item2));
+ Assert.assertFalse(item2.equals(item1));
+
+ item2.addListener(propertySetListenerMock2);
+
+ Assert.assertTrue(item1.equals(item2));
+ Assert.assertTrue(item2.equals(item1));
+ }
+
+ @Test
+ public void testEqualsAddRemoveListener() {
+ PropertysetItem item1 = createPropertySetItem();
+ PropertysetItem item2 = createPropertySetItem();
+
+ item1.addListener(propertySetListenerMock);
+ item1.removeListener(propertySetListenerMock);
+
+ Assert.assertTrue(item1.equals(item2));
+ Assert.assertTrue(item2.equals(item1));
+ }
+
+ @Test
+ public void testItemHashCodeEmpty() {
+ PropertysetItem item1 = createPropertySetItem();
+ PropertysetItem item2 = createPropertySetItem();
+
+ Assert.assertEquals(item1.hashCode(), item2.hashCode());
+ }
+
+ @Test
+ public void testItemHashCodeAddProperties() {
+ PropertysetItem item1 = createPropertySetItem();
+ PropertysetItem item2 = createPropertySetItem();
+
+ Assert.assertEquals(item1.hashCode(), item2.hashCode());
+
+ item1.addItemProperty(ID1, prop1);
+ item1.addItemProperty(ID2, prop2);
+ // hashCodes can be equal even if items are different
+
+ item2.addItemProperty(ID1, prop1);
+ item2.addItemProperty(ID2, prop2);
+ // but here hashCodes must be equal
+ Assert.assertEquals(item1.hashCode(), item2.hashCode());
+ }
+
+ @Test
+ public void testItemHashCodeAddListeners() {
+ PropertysetItem item1 = createPropertySetItem();
+ PropertysetItem item2 = createPropertySetItem();
+
+ Assert.assertEquals(item1.hashCode(), item2.hashCode());
+
+ item1.addListener(propertySetListenerMock);
+ // hashCodes can be equal even if items are different
+
+ item2.addListener(propertySetListenerMock);
+ // but here hashCodes must be equal
+ Assert.assertEquals(item1.hashCode(), item2.hashCode());
+ }
+
+ @Test
+ public void testItemHashCodeAddRemoveProperty() {
+ PropertysetItem item1 = createPropertySetItem();
+ PropertysetItem item2 = createPropertySetItem();
+
+ item1.addItemProperty(ID1, prop1);
+ item1.removeItemProperty(ID1);
+
+ Assert.assertEquals(item1.hashCode(), item2.hashCode());
+ }
+
+ @Test
+ public void testItemHashCodeAddRemoveListener() {
+ PropertysetItem item1 = createPropertySetItem();
+ PropertysetItem item2 = createPropertySetItem();
+
+ item1.addListener(propertySetListenerMock);
+ item1.removeListener(propertySetListenerMock);
+
+ Assert.assertEquals(item1.hashCode(), item2.hashCode());
+ }
+
+ @Test
+ public void testToString() {
+ // toString() behavior is specified in the class javadoc
+ PropertysetItem item = createPropertySetItem();
+
+ Assert.assertEquals("", item.toString());
+
+ item.addItemProperty(ID1, prop1);
+
+ Assert.assertEquals(String.valueOf(prop1.getValue()), item.toString());
+
+ item.addItemProperty(ID2, prop2);
+
+ Assert.assertEquals(String.valueOf(prop1.getValue()) + " "
+ + String.valueOf(prop2.getValue()), item.toString());
+ }
+
+}
diff --git a/compatibility-server/src/test/java/com/vaadin/data/util/ReflectToolsGetSuperFieldTest.java b/compatibility-server/src/test/java/com/vaadin/data/util/ReflectToolsGetSuperFieldTest.java
new file mode 100644
index 0000000000..df4258f316
--- /dev/null
+++ b/compatibility-server/src/test/java/com/vaadin/data/util/ReflectToolsGetSuperFieldTest.java
@@ -0,0 +1,35 @@
+package com.vaadin.data.util;
+
+import static org.junit.Assert.assertTrue;
+
+import org.junit.Test;
+
+import com.vaadin.data.fieldgroup.FieldGroup;
+import com.vaadin.data.fieldgroup.PropertyId;
+import com.vaadin.v7.ui.LegacyTextField;
+
+public class ReflectToolsGetSuperFieldTest {
+
+ @Test
+ public void getFieldFromSuperClass() {
+ class MyClass {
+ @PropertyId("testProperty")
+ LegacyTextField test = new LegacyTextField("This is a test");
+ }
+ class MySubClass extends MyClass {
+ // no fields here
+ }
+
+ PropertysetItem item = new PropertysetItem();
+ item.addItemProperty("testProperty",
+ new ObjectProperty<String>("Value of testProperty"));
+
+ MySubClass form = new MySubClass();
+
+ FieldGroup binder = new FieldGroup(item);
+ binder.bindMemberFields(form);
+
+ assertTrue("Value of testProperty".equals(form.test.getValue()));
+ }
+
+}
diff --git a/compatibility-server/src/test/java/com/vaadin/data/util/TransactionalPropertyWrapperTest.java b/compatibility-server/src/test/java/com/vaadin/data/util/TransactionalPropertyWrapperTest.java
new file mode 100644
index 0000000000..ef3e416f96
--- /dev/null
+++ b/compatibility-server/src/test/java/com/vaadin/data/util/TransactionalPropertyWrapperTest.java
@@ -0,0 +1,117 @@
+/*
+ * Copyright 2000-2016 Vaadin Ltd.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not
+ * use this file except in compliance with the License. You may obtain a copy of
+ * the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+package com.vaadin.data.util;
+
+import static org.junit.Assert.assertTrue;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import org.junit.Test;
+
+import com.vaadin.data.fieldgroup.FieldGroup;
+import com.vaadin.v7.ui.LegacyTextField;
+
+/**
+ * Test verifying that TransactionalPropertyWrapper removes it's listener from
+ * wrapped Property
+ *
+ * @since 7.1.15
+ * @author Vaadin Ltd
+ */
+public class TransactionalPropertyWrapperTest {
+
+ @SuppressWarnings("serial")
+ public class TestingProperty<T extends Object>
+ extends ObjectProperty<Object> {
+
+ private List<ValueChangeListener> listeners = new ArrayList<ValueChangeListener>();
+
+ public TestingProperty(Object value) {
+ super(value);
+ }
+
+ @Override
+ public void addValueChangeListener(ValueChangeListener listener) {
+ super.addValueChangeListener(listener);
+ listeners.add(listener);
+ }
+
+ @Override
+ public void removeValueChangeListener(ValueChangeListener listener) {
+ super.removeValueChangeListener(listener);
+ if (listeners.contains(listener)) {
+ listeners.remove(listener);
+ }
+ }
+
+ public boolean hasListeners() {
+ return !listeners.isEmpty();
+ }
+ }
+
+ private final LegacyTextField nameField = new LegacyTextField("Name");
+ private final LegacyTextField ageField = new LegacyTextField("Age");
+ private final LegacyTextField unboundField = new LegacyTextField(
+ "No FieldGroup");
+ private final TestingProperty<String> unboundProp = new TestingProperty<String>(
+ "Hello World");
+ private final PropertysetItem item = new PropertysetItem();
+
+ @Test
+ public void fieldGroupBindAndUnbind() {
+ item.addItemProperty("name",
+ new TestingProperty<String>("Just some text"));
+ item.addItemProperty("age", new TestingProperty<String>("42"));
+
+ final FieldGroup binder = new FieldGroup(item);
+ binder.setBuffered(false);
+
+ for (int i = 0; i < 2; ++i) {
+ binder.bind(nameField, "name");
+ binder.bind(ageField, "age");
+ unboundField.setPropertyDataSource(unboundProp);
+
+ assertTrue("No listeners in Properties", fieldsHaveListeners(true));
+
+ binder.unbind(nameField);
+ binder.unbind(ageField);
+ unboundField.setPropertyDataSource(null);
+
+ assertTrue("Listeners in Properties after unbinding",
+ fieldsHaveListeners(false));
+ }
+ }
+
+ /**
+ * Check that all listeners have same hasListeners() response
+ *
+ * @param expected
+ * expected response
+ * @return true if all are the same as expected. false if not
+ */
+ private boolean fieldsHaveListeners(boolean expected) {
+ for (Object id : item.getItemPropertyIds()) {
+ TestingProperty<?> itemProperty = (TestingProperty<?>) item
+ .getItemProperty(id);
+
+ if (itemProperty.hasListeners() != expected) {
+ return false;
+ }
+ }
+ return unboundProp.hasListeners() == expected;
+ }
+}
diff --git a/compatibility-server/src/test/java/com/vaadin/tests/server/ContextClickListenerTest.java b/compatibility-server/src/test/java/com/vaadin/tests/server/ContextClickListenerTest.java
new file mode 100644
index 0000000000..1b93a3063d
--- /dev/null
+++ b/compatibility-server/src/test/java/com/vaadin/tests/server/ContextClickListenerTest.java
@@ -0,0 +1,163 @@
+/*
+ * Copyright 2000-2016 Vaadin Ltd.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not
+ * use this file except in compliance with the License. You may obtain a copy of
+ * the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+package com.vaadin.tests.server;
+
+import java.util.EventObject;
+
+import org.easymock.EasyMock;
+import org.junit.Assert;
+import org.junit.Test;
+
+import com.vaadin.event.ContextClickEvent;
+import com.vaadin.event.ContextClickEvent.ContextClickListener;
+import com.vaadin.ui.AbstractComponent;
+import com.vaadin.ui.LegacyGrid.GridContextClickEvent;
+import com.vaadin.ui.Table.TableContextClickEvent;
+
+/**
+ * Server-side unit tests to see that context click events are sent to listeners
+ * correctly.
+ *
+ * If a listener is listening to a super type of an event, it should get the
+ * event. i.e. Listening to ContextClickEvent, it should get the specialized
+ * GridContextClickEvent as well.
+ *
+ * If a listener is listening to a sub-type of an event, it should not get the
+ * super version. i.e. Listening to GridContextClickEvent, it should not get a
+ * plain ContextClickEvent.
+ */
+public class ContextClickListenerTest extends AbstractComponent {
+
+ private final static ContextClickEvent contextClickEvent = EasyMock
+ .createMock(ContextClickEvent.class);
+ private final static GridContextClickEvent gridContextClickEvent = EasyMock
+ .createMock(GridContextClickEvent.class);
+ private final static TableContextClickEvent tableContextClickEvent = EasyMock
+ .createMock(TableContextClickEvent.class);
+
+ private final AssertListener contextListener = new AssertListener();
+ private final AssertListener ctxtListener2 = new AssertListener();
+
+ public static class AssertListener implements ContextClickListener {
+
+ private Class<?> expected = null;
+ private String error = null;
+
+ @Override
+ public void contextClick(ContextClickEvent event) {
+ if (expected == null) {
+ error = "Unexpected context click event.";
+ return;
+ }
+
+ if (!expected.isAssignableFrom(event.getClass())) {
+ error = "Expected event type did not match the actual event.";
+ }
+
+ expected = null;
+ }
+
+ public <T extends ContextClickEvent> void expect(Class<T> clazz) {
+ validate();
+ expected = clazz;
+ }
+
+ public void validate() {
+ if (expected != null) {
+ Assert.fail("Expected context click never happened.");
+ } else if (error != null) {
+ Assert.fail(error);
+ }
+ }
+ }
+
+ @Test
+ public void testListenerGetsASubClass() {
+ addContextClickListener(contextListener);
+ contextListener.expect(GridContextClickEvent.class);
+ fireEvent(gridContextClickEvent);
+ }
+
+ @Test
+ public void testListenerGetsExactClass() {
+ addContextClickListener(contextListener);
+ contextListener.expect(ContextClickEvent.class);
+ fireEvent(contextClickEvent);
+ }
+
+ /**
+ * Multiple listeners should get fitting events.
+ */
+ @Test
+ public void testMultipleListenerGetEvents() {
+ addContextClickListener(ctxtListener2);
+ addContextClickListener(contextListener);
+
+ ctxtListener2.expect(GridContextClickEvent.class);
+ contextListener.expect(GridContextClickEvent.class);
+
+ fireEvent(gridContextClickEvent);
+ }
+
+ @Test
+ public void testAddAndRemoveListener() {
+ addContextClickListener(contextListener);
+ contextListener.expect(ContextClickEvent.class);
+
+ fireEvent(contextClickEvent);
+
+ removeContextClickListener(contextListener);
+
+ fireEvent(contextClickEvent);
+ }
+
+ @Test
+ public void testAddAndRemoveMultipleListeners() {
+ addContextClickListener(ctxtListener2);
+ addContextClickListener(contextListener);
+
+ ctxtListener2.expect(GridContextClickEvent.class);
+ contextListener.expect(GridContextClickEvent.class);
+ fireEvent(gridContextClickEvent);
+
+ removeContextClickListener(ctxtListener2);
+
+ contextListener.expect(GridContextClickEvent.class);
+ fireEvent(gridContextClickEvent);
+ }
+
+ @Test(expected = AssertionError.class)
+ public void testExpectedEventNotReceived() {
+ addContextClickListener(contextListener);
+ contextListener.expect(GridContextClickEvent.class);
+ fireEvent(contextClickEvent);
+ }
+
+ @Test(expected = AssertionError.class)
+ public void testUnexpectedEventReceived() {
+ addContextClickListener(contextListener);
+ fireEvent(gridContextClickEvent);
+ }
+
+ @Override
+ protected void fireEvent(EventObject event) {
+ super.fireEvent(event);
+
+ // Validate listeners automatically.
+ ctxtListener2.validate();
+ contextListener.validate();
+ }
+}
diff --git a/compatibility-server/src/test/java/com/vaadin/tests/server/component/fieldgroup/BeanFieldGroupTest.java b/compatibility-server/src/test/java/com/vaadin/tests/server/component/fieldgroup/BeanFieldGroupTest.java
new file mode 100644
index 0000000000..f5c315b440
--- /dev/null
+++ b/compatibility-server/src/test/java/com/vaadin/tests/server/component/fieldgroup/BeanFieldGroupTest.java
@@ -0,0 +1,172 @@
+package com.vaadin.tests.server.component.fieldgroup;
+
+import static org.junit.Assert.assertEquals;
+
+import org.junit.Assert;
+import org.junit.Test;
+
+import com.vaadin.data.Item;
+import com.vaadin.data.fieldgroup.BeanFieldGroup;
+import com.vaadin.data.fieldgroup.FieldGroup.CommitException;
+import com.vaadin.data.fieldgroup.PropertyId;
+import com.vaadin.data.util.BeanItem;
+import com.vaadin.ui.RichTextArea;
+import com.vaadin.v7.ui.LegacyField;
+import com.vaadin.v7.ui.LegacyTextField;
+
+public class BeanFieldGroupTest {
+
+ private static final String DEFAULT_FOR_BASIC_FIELD = "default";
+
+ public static class MyBean {
+
+ private String basicField = DEFAULT_FOR_BASIC_FIELD;
+
+ private String anotherField;
+
+ private MyNestedBean nestedBean = new MyNestedBean();
+
+ public MyNestedBean getNestedBean() {
+ return nestedBean;
+ }
+
+ /**
+ * @return the basicField
+ */
+ public String getBasicField() {
+ return basicField;
+ }
+
+ /**
+ * @param basicField
+ * the basicField to set
+ */
+ public void setBasicField(String basicField) {
+ this.basicField = basicField;
+ }
+
+ /**
+ * @return the anotherField
+ */
+ public String getAnotherField() {
+ return anotherField;
+ }
+
+ /**
+ * @param anotherField
+ * the anotherField to set
+ */
+ public void setAnotherField(String anotherField) {
+ this.anotherField = anotherField;
+ }
+ }
+
+ public static class MyNestedBean {
+
+ private String hello = "Hello world";
+
+ public String getHello() {
+ return hello;
+ }
+ }
+
+ public static class ViewStub {
+
+ LegacyTextField basicField = new LegacyTextField();
+
+ @PropertyId("anotherField")
+ LegacyTextField boundWithAnnotation = new LegacyTextField();
+ }
+
+ @SuppressWarnings("unchecked")
+ @Test
+ public void testStaticBindingHelper() {
+ MyBean myBean = new MyBean();
+
+ ViewStub viewStub = new ViewStub();
+ BeanFieldGroup<MyBean> bindFields = BeanFieldGroup
+ .bindFieldsUnbuffered(myBean, viewStub);
+
+ LegacyField<String> field = (LegacyField<String>) bindFields
+ .getField("basicField");
+ Assert.assertEquals(DEFAULT_FOR_BASIC_FIELD, myBean.basicField);
+ field.setValue("Foo");
+ Assert.assertEquals("Foo", myBean.basicField);
+
+ field = (LegacyField<String>) bindFields.getField("anotherField");
+ field.setValue("Foo");
+ Assert.assertEquals("Foo", myBean.anotherField);
+ }
+
+ @SuppressWarnings("unchecked")
+ @Test
+ public void testStaticBufferedBindingHelper() throws CommitException {
+ MyBean myBean = new MyBean();
+
+ ViewStub viewStub = new ViewStub();
+ BeanFieldGroup<MyBean> bindFields = BeanFieldGroup
+ .bindFieldsBuffered(myBean, viewStub);
+
+ LegacyField<String> basicField = (LegacyField<String>) bindFields
+ .getField("basicField");
+ basicField.setValue("Foo");
+ Assert.assertEquals(DEFAULT_FOR_BASIC_FIELD, myBean.basicField);
+
+ LegacyField<String> anotherField = (LegacyField<String>) bindFields
+ .getField("anotherField");
+ anotherField.setValue("Foo");
+ Assert.assertNull(myBean.anotherField);
+
+ bindFields.commit();
+
+ Assert.assertEquals("Foo", myBean.basicField);
+ Assert.assertEquals("Foo", myBean.anotherField);
+
+ }
+
+ @Test
+ public void buildAndBindNestedProperty() {
+
+ MyBean bean = new MyBean();
+
+ BeanFieldGroup<MyBean> bfg = new BeanFieldGroup<MyBean>(MyBean.class);
+ bfg.setItemDataSource(bean);
+
+ com.vaadin.v7.ui.LegacyField<?> helloField = bfg
+ .buildAndBind("Hello string", "nestedBean.hello");
+ assertEquals(bean.nestedBean.hello, helloField.getValue().toString());
+ }
+
+ @Test
+ public void buildAndBindNestedRichTextAreaProperty() {
+
+ MyBean bean = new MyBean();
+
+ BeanFieldGroup<MyBean> bfg = new BeanFieldGroup<MyBean>(MyBean.class);
+ bfg.setItemDataSource(bean);
+
+ RichTextArea helloField = bfg.buildAndBind("Hello string",
+ "nestedBean.hello", RichTextArea.class);
+ assertEquals(bean.nestedBean.hello, helloField.getValue().toString());
+ }
+
+ @Test
+ public void setDataSource_nullBean_nullBeanIsSetInDataSource() {
+ BeanFieldGroup<MyBean> group = new BeanFieldGroup<MyBean>(MyBean.class);
+
+ group.setItemDataSource((MyBean) null);
+
+ BeanItem<MyBean> dataSource = group.getItemDataSource();
+ Assert.assertNull("Data source is null for null bean", dataSource);
+ }
+
+ @Test
+ public void setDataSource_nullItem_nullDataSourceIsSet() {
+ BeanFieldGroup<MyBean> group = new BeanFieldGroup<MyBean>(MyBean.class);
+
+ group.setItemDataSource((Item) null);
+ BeanItem<MyBean> dataSource = group.getItemDataSource();
+ Assert.assertNull("Group returns not null data source", dataSource);
+ }
+
+}
diff --git a/compatibility-server/src/test/java/com/vaadin/tests/server/component/fieldgroup/CaseInsensitiveBindingTest.java b/compatibility-server/src/test/java/com/vaadin/tests/server/component/fieldgroup/CaseInsensitiveBindingTest.java
new file mode 100644
index 0000000000..a324c02cfc
--- /dev/null
+++ b/compatibility-server/src/test/java/com/vaadin/tests/server/component/fieldgroup/CaseInsensitiveBindingTest.java
@@ -0,0 +1,85 @@
+package com.vaadin.tests.server.component.fieldgroup;
+
+import static org.junit.Assert.assertTrue;
+
+import org.junit.Test;
+
+import com.vaadin.data.fieldgroup.FieldGroup;
+import com.vaadin.data.util.ObjectProperty;
+import com.vaadin.data.util.PropertysetItem;
+import com.vaadin.ui.FormLayout;
+import com.vaadin.v7.ui.LegacyTextField;
+
+public class CaseInsensitiveBindingTest {
+
+ @Test
+ public void caseInsensitivityAndUnderscoreRemoval() {
+ PropertysetItem item = new PropertysetItem();
+ item.addItemProperty("LastName", new ObjectProperty<String>("Sparrow"));
+
+ class MyForm extends FormLayout {
+ LegacyTextField lastName = new LegacyTextField("Last name");
+
+ public MyForm() {
+
+ // Should bind to the LastName property
+ addComponent(lastName);
+ }
+ }
+
+ MyForm form = new MyForm();
+
+ FieldGroup binder = new FieldGroup(item);
+ binder.bindMemberFields(form);
+
+ assertTrue("Sparrow".equals(form.lastName.getValue()));
+ }
+
+ @Test
+ public void UnderscoreRemoval() {
+ PropertysetItem item = new PropertysetItem();
+ item.addItemProperty("first_name", new ObjectProperty<String>("Jack"));
+
+ class MyForm extends FormLayout {
+ LegacyTextField firstName = new LegacyTextField("First name");
+
+ public MyForm() {
+ // Should bind to the first_name property
+ addComponent(firstName);
+ }
+ }
+
+ MyForm form = new MyForm();
+
+ FieldGroup binder = new FieldGroup(item);
+ binder.bindMemberFields(form);
+
+ assertTrue("Jack".equals(form.firstName.getValue()));
+ }
+
+ @Test
+ public void perfectMatchPriority() {
+ PropertysetItem item = new PropertysetItem();
+ item.addItemProperty("first_name",
+ new ObjectProperty<String>("Not this"));
+ item.addItemProperty("firstName", new ObjectProperty<String>("This"));
+
+ class MyForm extends FormLayout {
+ LegacyTextField firstName = new LegacyTextField("First name");
+
+ public MyForm() {
+ // should bind to the firstName property, not first_name
+ // property
+ addComponent(firstName);
+ }
+ }
+
+ MyForm form = new MyForm();
+
+ FieldGroup binder = new FieldGroup(item);
+ binder.bindMemberFields(form);
+
+ assertTrue("This".equals(form.firstName.getValue()));
+ }
+
+}
diff --git a/compatibility-server/src/test/java/com/vaadin/tests/server/component/fieldgroup/FieldGroupTest.java b/compatibility-server/src/test/java/com/vaadin/tests/server/component/fieldgroup/FieldGroupTest.java
new file mode 100644
index 0000000000..43d2bf8a6a
--- /dev/null
+++ b/compatibility-server/src/test/java/com/vaadin/tests/server/component/fieldgroup/FieldGroupTest.java
@@ -0,0 +1,149 @@
+/*
+ * Copyright 2000-2016 Vaadin Ltd.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not
+ * use this file except in compliance with the License. You may obtain a copy of
+ * the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+package com.vaadin.tests.server.component.fieldgroup;
+
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.HashSet;
+import java.util.Map;
+import java.util.Map.Entry;
+import java.util.Set;
+
+import org.junit.Assert;
+import org.junit.Test;
+
+import com.vaadin.data.Item;
+import com.vaadin.data.Property;
+import com.vaadin.data.fieldgroup.FieldGroup;
+import com.vaadin.data.fieldgroup.FieldGroup.CommitException;
+import com.vaadin.data.util.AbstractProperty;
+import com.vaadin.v7.data.Validator.InvalidValueException;
+import com.vaadin.v7.ui.LegacyField;
+import com.vaadin.v7.ui.LegacyTextField;
+
+/**
+ *
+ * Tests for {@link FieldGroup}.
+ *
+ * @author Vaadin Ltd
+ */
+public class FieldGroupTest {
+
+ @Test
+ public void setReadOnly_readOnlyAndNoDataSource_fieldIsReadOnly() {
+ FieldGroup fieldGroup = new FieldGroup();
+
+ LegacyTextField field = new LegacyTextField();
+ fieldGroup.bind(field, "property");
+
+ fieldGroup.setReadOnly(true);
+
+ Assert.assertTrue("Field is not read only", field.isReadOnly());
+ }
+
+ @Test
+ public void setReadOnly_writableAndNoDataSource_fieldIsWritable() {
+ FieldGroup fieldGroup = new FieldGroup();
+
+ LegacyTextField field = new LegacyTextField();
+ fieldGroup.bind(field, "property");
+
+ fieldGroup.setReadOnly(false);
+
+ Assert.assertFalse("Field is not writable", field.isReadOnly());
+ }
+
+ @Test
+ public void commit_validationFailed_allValidationFailuresAvailable()
+ throws CommitException {
+ FieldGroup fieldGroup = new FieldGroup();
+
+ fieldGroup.setItemDataSource(new TestItem());
+
+ LegacyTextField field1 = new LegacyTextField();
+ field1.setRequired(true);
+ fieldGroup.bind(field1, "prop1");
+
+ LegacyTextField field2 = new LegacyTextField();
+ field2.setRequired(true);
+ fieldGroup.bind(field2, "prop2");
+
+ Set<LegacyTextField> set = new HashSet<>(Arrays.asList(field1, field2));
+
+ try {
+ fieldGroup.commit();
+ Assert.fail("No commit exception is thrown");
+ } catch (CommitException exception) {
+ Map<LegacyField<?>, ? extends InvalidValueException> invalidFields = exception
+ .getInvalidFields();
+ for (Entry<LegacyField<?>, ? extends InvalidValueException> entry : invalidFields
+ .entrySet()) {
+ set.remove(entry.getKey());
+ }
+ Assert.assertEquals(
+ "Some fields are not found in the invalid fields map", 0,
+ set.size());
+ Assert.assertEquals(
+ "Invalid value exception should be thrown for each field",
+ 2, invalidFields.size());
+ }
+ }
+
+ private static class TestItem implements Item {
+
+ @Override
+ public Property<String> getItemProperty(Object id) {
+ return new StringProperty();
+ }
+
+ @Override
+ public Collection<?> getItemPropertyIds() {
+ return Arrays.asList("prop1", "prop2");
+ }
+
+ @Override
+ public boolean addItemProperty(Object id, Property property)
+ throws UnsupportedOperationException {
+ return false;
+ }
+
+ @Override
+ public boolean removeItemProperty(Object id)
+ throws UnsupportedOperationException {
+ return false;
+ }
+
+ }
+
+ private static class StringProperty extends AbstractProperty<String> {
+
+ @Override
+ public String getValue() {
+ return null;
+ }
+
+ @Override
+ public void setValue(String newValue)
+ throws Property.ReadOnlyException {
+ }
+
+ @Override
+ public Class<? extends String> getType() {
+ return String.class;
+ }
+ }
+
+}
diff --git a/compatibility-server/src/test/java/com/vaadin/tests/server/component/fieldgroup/FieldGroupWithReadOnlyPropertiesTest.java b/compatibility-server/src/test/java/com/vaadin/tests/server/component/fieldgroup/FieldGroupWithReadOnlyPropertiesTest.java
new file mode 100644
index 0000000000..f14b966a2d
--- /dev/null
+++ b/compatibility-server/src/test/java/com/vaadin/tests/server/component/fieldgroup/FieldGroupWithReadOnlyPropertiesTest.java
@@ -0,0 +1,51 @@
+package com.vaadin.tests.server.component.fieldgroup;
+
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
+
+import org.junit.Test;
+
+import com.vaadin.data.fieldgroup.FieldGroup;
+import com.vaadin.data.util.BeanItem;
+import com.vaadin.tests.data.bean.BeanWithReadOnlyField;
+import com.vaadin.v7.ui.LegacyTextField;
+
+public class FieldGroupWithReadOnlyPropertiesTest {
+
+ private LegacyTextField readOnlyField = new LegacyTextField();
+ private LegacyTextField writableField = new LegacyTextField();
+
+ @Test
+ public void bindReadOnlyPropertyToFieldGroup() {
+ BeanWithReadOnlyField bean = new BeanWithReadOnlyField();
+ BeanItem<BeanWithReadOnlyField> beanItem = new BeanItem<BeanWithReadOnlyField>(
+ bean);
+ beanItem.getItemProperty("readOnlyField").setReadOnly(true);
+
+ FieldGroup fieldGroup = new FieldGroup(beanItem);
+ fieldGroup.bindMemberFields(this);
+
+ assertTrue(readOnlyField.isReadOnly());
+ assertFalse(writableField.isReadOnly());
+ }
+
+ @Test
+ public void fieldGroupSetReadOnlyTest() {
+ BeanWithReadOnlyField bean = new BeanWithReadOnlyField();
+ BeanItem<BeanWithReadOnlyField> beanItem = new BeanItem<BeanWithReadOnlyField>(
+ bean);
+ beanItem.getItemProperty("readOnlyField").setReadOnly(true);
+
+ FieldGroup fieldGroup = new FieldGroup(beanItem);
+ fieldGroup.bindMemberFields(this);
+
+ fieldGroup.setReadOnly(true);
+ assertTrue(readOnlyField.isReadOnly());
+ assertTrue(writableField.isReadOnly());
+
+ fieldGroup.setReadOnly(false);
+ assertTrue(readOnlyField.isReadOnly());
+ assertFalse(writableField.isReadOnly());
+ }
+
+}
diff --git a/compatibility-server/src/test/java/com/vaadin/tests/server/component/fieldgroup/FieldNamedDescriptionTest.java b/compatibility-server/src/test/java/com/vaadin/tests/server/component/fieldgroup/FieldNamedDescriptionTest.java
new file mode 100644
index 0000000000..0ef30316ac
--- /dev/null
+++ b/compatibility-server/src/test/java/com/vaadin/tests/server/component/fieldgroup/FieldNamedDescriptionTest.java
@@ -0,0 +1,53 @@
+package com.vaadin.tests.server.component.fieldgroup;
+
+import static org.junit.Assert.assertTrue;
+
+import org.junit.Test;
+
+import com.vaadin.data.fieldgroup.FieldGroup;
+import com.vaadin.data.fieldgroup.PropertyId;
+import com.vaadin.data.util.ObjectProperty;
+import com.vaadin.data.util.PropertysetItem;
+import com.vaadin.ui.FormLayout;
+import com.vaadin.v7.ui.LegacyTextField;
+
+public class FieldNamedDescriptionTest {
+
+ @Test
+ public void bindReadOnlyPropertyToFieldGroup() {
+ // Create an item
+ PropertysetItem item = new PropertysetItem();
+ item.addItemProperty("name", new ObjectProperty<String>("Zaphod"));
+ item.addItemProperty("description",
+ new ObjectProperty<String>("This is a description"));
+
+ // Define a form as a class that extends some layout
+ class MyForm extends FormLayout {
+ // Member that will bind to the "name" property
+ LegacyTextField name = new LegacyTextField("Name");
+
+ // This member will not bind to the desctiptionProperty as the name
+ // description conflicts with something in the binding process
+ @PropertyId("description")
+ LegacyTextField description = new LegacyTextField("Description");
+
+ public MyForm() {
+
+ // Add the fields
+ addComponent(name);
+ addComponent(description);
+ }
+ }
+
+ // Create one
+ MyForm form = new MyForm();
+
+ // Now create a binder that can also creates the fields
+ // using the default field factory
+ FieldGroup binder = new FieldGroup(item);
+ binder.bindMemberFields(form);
+
+ assertTrue(form.description.getValue().equals("This is a description"));
+ }
+
+}
diff --git a/compatibility-server/src/test/java/com/vaadin/tests/server/component/grid/GridAddRowBuiltinContainerTest.java b/compatibility-server/src/test/java/com/vaadin/tests/server/component/grid/GridAddRowBuiltinContainerTest.java
new file mode 100644
index 0000000000..3bff93c042
--- /dev/null
+++ b/compatibility-server/src/test/java/com/vaadin/tests/server/component/grid/GridAddRowBuiltinContainerTest.java
@@ -0,0 +1,219 @@
+/*
+ * Copyright 2000-2016 Vaadin Ltd.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not
+ * use this file except in compliance with the License. You may obtain a copy of
+ * the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+package com.vaadin.tests.server.component.grid;
+
+import org.junit.Assert;
+import org.junit.Before;
+import org.junit.Test;
+
+import com.vaadin.data.Container;
+import com.vaadin.data.Item;
+import com.vaadin.data.util.BeanItem;
+import com.vaadin.data.util.BeanItemContainer;
+import com.vaadin.data.util.MethodProperty.MethodException;
+import com.vaadin.tests.data.bean.Person;
+import com.vaadin.ui.LegacyGrid;
+
+public class GridAddRowBuiltinContainerTest {
+ LegacyGrid grid = new LegacyGrid();
+ Container.Indexed container;
+
+ @Before
+ public void setUp() {
+ container = grid.getContainerDataSource();
+
+ grid.addColumn("myColumn");
+ }
+
+ @Test
+ public void testSimpleCase() {
+ Object itemId = grid.addRow("Hello");
+
+ Assert.assertEquals(Integer.valueOf(1), itemId);
+
+ Assert.assertEquals("There should be one item in the container", 1,
+ container.size());
+
+ Assert.assertEquals("Hello", container.getItem(itemId)
+ .getItemProperty("myColumn").getValue());
+ }
+
+ @Test(expected = IllegalArgumentException.class)
+ public void testNullParameter() {
+ // cast to Object[] to distinguish from one null varargs value
+ grid.addRow((Object[]) null);
+ }
+
+ @Test
+ public void testNullValue() {
+ // cast to Object to distinguish from a null varargs array
+ Object itemId = grid.addRow((Object) null);
+
+ Assert.assertEquals(null, container.getItem(itemId)
+ .getItemProperty("myColumn").getValue());
+ }
+
+ @Test(expected = IllegalArgumentException.class)
+ public void testAddInvalidType() {
+ grid.addRow(Integer.valueOf(5));
+ }
+
+ @Test
+ public void testMultipleProperties() {
+ grid.addColumn("myOther", Integer.class);
+
+ Object itemId = grid.addRow("Hello", Integer.valueOf(3));
+
+ Item item = container.getItem(itemId);
+ Assert.assertEquals("Hello",
+ item.getItemProperty("myColumn").getValue());
+ Assert.assertEquals(Integer.valueOf(3),
+ item.getItemProperty("myOther").getValue());
+ }
+
+ @Test(expected = IllegalArgumentException.class)
+ public void testInvalidPropertyAmount() {
+ grid.addRow("Hello", Integer.valueOf(3));
+ }
+
+ @Test
+ public void testRemovedColumn() {
+ grid.addColumn("myOther", Integer.class);
+ grid.removeColumn("myColumn");
+
+ grid.addRow(Integer.valueOf(3));
+
+ Item item = container.getItem(Integer.valueOf(1));
+ Assert.assertEquals("Default value should be used for removed column",
+ "", item.getItemProperty("myColumn").getValue());
+ Assert.assertEquals(Integer.valueOf(3),
+ item.getItemProperty("myOther").getValue());
+ }
+
+ @Test
+ public void testMultiplePropertiesAfterReorder() {
+ grid.addColumn("myOther", Integer.class);
+
+ grid.setColumnOrder("myOther", "myColumn");
+
+ grid.addRow(Integer.valueOf(3), "Hello");
+
+ Item item = container.getItem(Integer.valueOf(1));
+ Assert.assertEquals("Hello",
+ item.getItemProperty("myColumn").getValue());
+ Assert.assertEquals(Integer.valueOf(3),
+ item.getItemProperty("myOther").getValue());
+ }
+
+ @Test
+ public void testInvalidType_NothingAdded() {
+ try {
+ grid.addRow(Integer.valueOf(5));
+
+ // Can't use @Test(expect = Foo.class) since we also want to verify
+ // state after exception was thrown
+ Assert.fail("Adding wrong type should throw ClassCastException");
+ } catch (IllegalArgumentException e) {
+ Assert.assertEquals("No row should have been added", 0,
+ container.size());
+ }
+ }
+
+ @Test
+ public void testUnsupportingContainer() {
+ setContainerRemoveColumns(new BeanItemContainer<Person>(Person.class));
+ try {
+
+ grid.addRow("name");
+
+ // Can't use @Test(expect = Foo.class) since we also want to verify
+ // state after exception was thrown
+ Assert.fail(
+ "Adding to BeanItemContainer container should throw UnsupportedOperationException");
+ } catch (UnsupportedOperationException e) {
+ Assert.assertEquals("No row should have been added", 0,
+ container.size());
+ }
+ }
+
+ @Test
+ public void testCustomContainer() {
+ BeanItemContainer<Person> container = new BeanItemContainer<Person>(
+ Person.class) {
+ @Override
+ public Object addItem() {
+ BeanItem<Person> item = addBean(new Person());
+ return getBeanIdResolver().getIdForBean(item.getBean());
+ }
+ };
+
+ setContainerRemoveColumns(container);
+
+ grid.addRow("name");
+
+ Assert.assertEquals(1, container.size());
+
+ Assert.assertEquals("name", container.getIdByIndex(0).getFirstName());
+ }
+
+ @Test
+ public void testSetterThrowing() {
+ BeanItemContainer<Person> container = new BeanItemContainer<Person>(
+ Person.class) {
+ @Override
+ public Object addItem() {
+ BeanItem<Person> item = addBean(new Person() {
+ @Override
+ public void setFirstName(String firstName) {
+ if ("name".equals(firstName)) {
+ throw new RuntimeException(firstName);
+ } else {
+ super.setFirstName(firstName);
+ }
+ }
+ });
+ return getBeanIdResolver().getIdForBean(item.getBean());
+ }
+ };
+
+ setContainerRemoveColumns(container);
+
+ try {
+
+ grid.addRow("name");
+
+ // Can't use @Test(expect = Foo.class) since we also want to verify
+ // state after exception was thrown
+ Assert.fail("Adding row should throw MethodException");
+ } catch (MethodException e) {
+ Assert.assertEquals("Got the wrong exception", "name",
+ e.getCause().getMessage());
+
+ Assert.assertEquals("There should be no rows in the container", 0,
+ container.size());
+ }
+ }
+
+ private void setContainerRemoveColumns(
+ BeanItemContainer<Person> container) {
+ // Remove predefined column so we can change container
+ grid.removeAllColumns();
+ grid.setContainerDataSource(container);
+ grid.removeAllColumns();
+ grid.addColumn("firstName");
+ }
+
+}
diff --git a/compatibility-server/src/test/java/com/vaadin/tests/server/component/grid/GridChildrenTest.java b/compatibility-server/src/test/java/com/vaadin/tests/server/component/grid/GridChildrenTest.java
new file mode 100644
index 0000000000..f126e636ba
--- /dev/null
+++ b/compatibility-server/src/test/java/com/vaadin/tests/server/component/grid/GridChildrenTest.java
@@ -0,0 +1,59 @@
+/*
+ * Copyright 2000-2016 Vaadin Ltd.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not
+ * use this file except in compliance with the License. You may obtain a copy of
+ * the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+package com.vaadin.tests.server.component.grid;
+
+import java.util.Iterator;
+
+import com.vaadin.ui.LegacyGrid;
+import org.junit.Assert;
+import org.junit.Test;
+
+import com.vaadin.ui.Component;
+import com.vaadin.ui.LegacyGrid.FooterCell;
+import com.vaadin.ui.LegacyGrid.HeaderCell;
+import com.vaadin.ui.Label;
+
+public class GridChildrenTest {
+
+ @Test
+ public void componentsInMergedHeader() {
+ LegacyGrid grid = new LegacyGrid();
+ grid.addColumn("foo");
+ grid.addColumn("bar");
+ grid.addColumn("baz");
+ HeaderCell merged = grid.getDefaultHeaderRow().join("foo", "bar",
+ "baz");
+ Label label = new Label();
+ merged.setComponent(label);
+ Iterator<Component> i = grid.iterator();
+ Assert.assertEquals(label, i.next());
+ Assert.assertFalse(i.hasNext());
+ }
+
+ @Test
+ public void componentsInMergedFooter() {
+ LegacyGrid grid = new LegacyGrid();
+ grid.addColumn("foo");
+ grid.addColumn("bar");
+ grid.addColumn("baz");
+ FooterCell merged = grid.addFooterRowAt(0).join("foo", "bar", "baz");
+ Label label = new Label();
+ merged.setComponent(label);
+ Iterator<Component> i = grid.iterator();
+ Assert.assertEquals(label, i.next());
+ Assert.assertFalse(i.hasNext());
+ }
+}
diff --git a/compatibility-server/src/test/java/com/vaadin/tests/server/component/grid/GridColumnAddingAndRemovingTest.java b/compatibility-server/src/test/java/com/vaadin/tests/server/component/grid/GridColumnAddingAndRemovingTest.java
new file mode 100644
index 0000000000..1af2577fa3
--- /dev/null
+++ b/compatibility-server/src/test/java/com/vaadin/tests/server/component/grid/GridColumnAddingAndRemovingTest.java
@@ -0,0 +1,134 @@
+/*
+ * Copyright 2000-2016 Vaadin Ltd.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not
+ * use this file except in compliance with the License. You may obtain a copy of
+ * the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+package com.vaadin.tests.server.component.grid;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertNull;
+
+import com.vaadin.ui.LegacyGrid;
+import org.junit.Before;
+import org.junit.Test;
+
+import com.vaadin.data.Container;
+import com.vaadin.data.Property;
+import com.vaadin.data.util.IndexedContainer;
+
+public class GridColumnAddingAndRemovingTest {
+
+ LegacyGrid grid = new LegacyGrid();
+ Container.Indexed container;
+
+ @Before
+ public void setUp() {
+ container = grid.getContainerDataSource();
+ container.addItem();
+ }
+
+ @Test
+ public void testAddColumn() {
+ grid.addColumn("foo");
+
+ Property<?> property = container
+ .getContainerProperty(container.firstItemId(), "foo");
+ assertEquals(property.getType(), String.class);
+ }
+
+ @Test(expected = IllegalStateException.class)
+ public void testAddColumnTwice() {
+ grid.addColumn("foo");
+ grid.addColumn("foo");
+ }
+
+ @Test
+ public void testAddRemoveAndAddAgainColumn() {
+ grid.addColumn("foo");
+ grid.removeColumn("foo");
+
+ // Removing a column, doesn't remove the property
+ Property<?> property = container
+ .getContainerProperty(container.firstItemId(), "foo");
+ assertEquals(property.getType(), String.class);
+ grid.addColumn("foo");
+ }
+
+ @Test
+ public void testAddNumberColumns() {
+ grid.addColumn("bar", Integer.class);
+ grid.addColumn("baz", Double.class);
+
+ Property<?> property = container
+ .getContainerProperty(container.firstItemId(), "bar");
+ assertEquals(property.getType(), Integer.class);
+ assertEquals(null, property.getValue());
+ property = container.getContainerProperty(container.firstItemId(),
+ "baz");
+ assertEquals(property.getType(), Double.class);
+ assertEquals(null, property.getValue());
+ }
+
+ @Test(expected = IllegalStateException.class)
+ public void testAddDifferentTypeColumn() {
+ grid.addColumn("foo");
+ grid.removeColumn("foo");
+ grid.addColumn("foo", Integer.class);
+ }
+
+ @Test(expected = IllegalStateException.class)
+ public void testAddColumnToNonDefaultContainer() {
+ grid.setContainerDataSource(new IndexedContainer());
+ grid.addColumn("foo");
+ }
+
+ @Test
+ public void testAddColumnForExistingProperty() {
+ grid.addColumn("bar");
+ IndexedContainer container2 = new IndexedContainer();
+ container2.addContainerProperty("foo", Integer.class, 0);
+ container2.addContainerProperty("bar", String.class, "");
+ grid.setContainerDataSource(container2);
+ assertNull("Grid should not have a column for property foo",
+ grid.getColumn("foo"));
+ assertNotNull("Grid did should have a column for property bar",
+ grid.getColumn("bar"));
+ for (LegacyGrid.Column column : grid.getColumns()) {
+ assertNotNull("Grid getColumns returned a null value", column);
+ }
+
+ grid.removeAllColumns();
+ grid.addColumn("foo");
+ assertNotNull("Grid should now have a column for property foo",
+ grid.getColumn("foo"));
+ assertNull("Grid should not have a column for property bar anymore",
+ grid.getColumn("bar"));
+ }
+
+ @Test(expected = IllegalStateException.class)
+ public void testAddIncompatibleColumnProperty() {
+ grid.addColumn("bar");
+ grid.removeAllColumns();
+ grid.addColumn("bar", Integer.class);
+ }
+
+ @Test
+ public void testAddBooleanColumnProperty() {
+ grid.addColumn("foo", Boolean.class);
+ Property<?> property = container
+ .getContainerProperty(container.firstItemId(), "foo");
+ assertEquals(property.getType(), Boolean.class);
+ assertEquals(property.getValue(), null);
+ }
+}
diff --git a/compatibility-server/src/test/java/com/vaadin/tests/server/component/grid/GridColumnsTest.java b/compatibility-server/src/test/java/com/vaadin/tests/server/component/grid/GridColumnsTest.java
new file mode 100644
index 0000000000..570e9b92fc
--- /dev/null
+++ b/compatibility-server/src/test/java/com/vaadin/tests/server/component/grid/GridColumnsTest.java
@@ -0,0 +1,413 @@
+/*
+ * Copyright 2000-2016 Vaadin Ltd.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not
+ * use this file except in compliance with the License. You may obtain a copy of
+ * the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+package com.vaadin.tests.server.component.grid;
+
+import static org.easymock.EasyMock.and;
+import static org.easymock.EasyMock.capture;
+import static org.easymock.EasyMock.isA;
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.hamcrest.core.Is.is;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertSame;
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.fail;
+
+import java.lang.reflect.Field;
+import java.lang.reflect.Method;
+import java.util.Iterator;
+import java.util.LinkedHashSet;
+import java.util.Set;
+
+import com.vaadin.ui.LegacyGrid;
+import org.easymock.Capture;
+import org.easymock.EasyMock;
+import org.junit.Before;
+import org.junit.Test;
+
+import com.vaadin.data.util.IndexedContainer;
+import com.vaadin.server.KeyMapper;
+import com.vaadin.shared.ui.grid.GridColumnState;
+import com.vaadin.shared.ui.grid.GridState;
+import com.vaadin.shared.util.SharedUtil;
+import com.vaadin.ui.LegacyGrid.Column;
+import com.vaadin.ui.LegacyGrid.ColumnResizeEvent;
+import com.vaadin.ui.LegacyGrid.ColumnResizeListener;
+import com.vaadin.v7.ui.LegacyTextField;
+
+public class GridColumnsTest {
+
+ private LegacyGrid grid;
+
+ private GridState state;
+
+ private Method getStateMethod;
+
+ private Field columnIdGeneratorField;
+
+ private KeyMapper<Object> columnIdMapper;
+
+ @Before
+ @SuppressWarnings("unchecked")
+ public void setup() throws Exception {
+ IndexedContainer ds = new IndexedContainer();
+ for (int c = 0; c < 10; c++) {
+ ds.addContainerProperty("column" + c, String.class, "");
+ }
+ ds.addContainerProperty("noSort", Object.class, null);
+ grid = new LegacyGrid(ds);
+
+ getStateMethod = LegacyGrid.class.getDeclaredMethod("getState");
+ getStateMethod.setAccessible(true);
+
+ state = (GridState) getStateMethod.invoke(grid);
+
+ columnIdGeneratorField = LegacyGrid.class.getDeclaredField("columnKeys");
+ columnIdGeneratorField.setAccessible(true);
+
+ columnIdMapper = (KeyMapper<Object>) columnIdGeneratorField.get(grid);
+ }
+
+ @Test
+ public void testColumnGeneration() throws Exception {
+
+ for (Object propertyId : grid.getContainerDataSource()
+ .getContainerPropertyIds()) {
+
+ // All property ids should get a column
+ Column column = grid.getColumn(propertyId);
+ assertNotNull(column);
+
+ // Humanized property id should be the column header by default
+ assertEquals(
+ SharedUtil.camelCaseToHumanFriendly(propertyId.toString()),
+ grid.getDefaultHeaderRow().getCell(propertyId).getText());
+ }
+ }
+
+ @Test
+ public void testModifyingColumnProperties() throws Exception {
+
+ // Modify first column
+ Column column = grid.getColumn("column1");
+ assertNotNull(column);
+
+ column.setHeaderCaption("CustomHeader");
+ assertEquals("CustomHeader", column.getHeaderCaption());
+ assertEquals(column.getHeaderCaption(),
+ grid.getDefaultHeaderRow().getCell("column1").getText());
+
+ column.setWidth(100);
+ assertEquals(100, column.getWidth(), 0.49d);
+ assertEquals(column.getWidth(), getColumnState("column1").width, 0.49d);
+
+ try {
+ column.setWidth(-1);
+ fail("Setting width to -1 should throw exception");
+ } catch (IllegalArgumentException iae) {
+ // expected
+ }
+
+ assertEquals(100, column.getWidth(), 0.49d);
+ assertEquals(100, getColumnState("column1").width, 0.49d);
+ }
+
+ @Test
+ public void testRemovingColumnByRemovingPropertyFromContainer()
+ throws Exception {
+
+ Column column = grid.getColumn("column1");
+ assertNotNull(column);
+
+ // Remove column
+ grid.getContainerDataSource().removeContainerProperty("column1");
+
+ try {
+ column.setHeaderCaption("asd");
+
+ fail("Succeeded in modifying a detached column");
+ } catch (IllegalStateException ise) {
+ // Detached state should throw exception
+ }
+
+ try {
+ column.setWidth(123);
+ fail("Succeeded in modifying a detached column");
+ } catch (IllegalStateException ise) {
+ // Detached state should throw exception
+ }
+
+ assertNull(grid.getColumn("column1"));
+ assertNull(getColumnState("column1"));
+ }
+
+ @Test
+ public void testAddingColumnByAddingPropertyToContainer() throws Exception {
+ grid.getContainerDataSource().addContainerProperty("columnX",
+ String.class, "");
+ Column column = grid.getColumn("columnX");
+ assertNotNull(column);
+ }
+
+ @Test
+ public void testHeaderVisiblility() throws Exception {
+
+ assertTrue(grid.isHeaderVisible());
+ assertTrue(state.header.visible);
+
+ grid.setHeaderVisible(false);
+ assertFalse(grid.isHeaderVisible());
+ assertFalse(state.header.visible);
+
+ grid.setHeaderVisible(true);
+ assertTrue(grid.isHeaderVisible());
+ assertTrue(state.header.visible);
+ }
+
+ @Test
+ public void testFooterVisibility() throws Exception {
+
+ assertTrue(grid.isFooterVisible());
+ assertTrue(state.footer.visible);
+
+ grid.setFooterVisible(false);
+ assertFalse(grid.isFooterVisible());
+ assertFalse(state.footer.visible);
+
+ grid.setFooterVisible(true);
+ assertTrue(grid.isFooterVisible());
+ assertTrue(state.footer.visible);
+ }
+
+ @Test
+ public void testSetFrozenColumnCount() {
+ assertEquals("Grid should not start with a frozen column", 0,
+ grid.getFrozenColumnCount());
+ grid.setFrozenColumnCount(2);
+ assertEquals("Freezing two columns should freeze two columns", 2,
+ grid.getFrozenColumnCount());
+ }
+
+ @Test
+ public void testSetFrozenColumnCountThroughColumn() {
+ assertEquals("Grid should not start with a frozen column", 0,
+ grid.getFrozenColumnCount());
+ grid.getColumns().get(2).setLastFrozenColumn();
+ assertEquals(
+ "Setting the third column as last frozen should freeze three columns",
+ 3, grid.getFrozenColumnCount());
+ }
+
+ @Test
+ public void testFrozenColumnRemoveColumn() {
+ assertEquals("Grid should not start with a frozen column", 0,
+ grid.getFrozenColumnCount());
+
+ int containerSize = grid.getContainerDataSource()
+ .getContainerPropertyIds().size();
+ grid.setFrozenColumnCount(containerSize);
+
+ Object propertyId = grid.getContainerDataSource()
+ .getContainerPropertyIds().iterator().next();
+
+ grid.getContainerDataSource().removeContainerProperty(propertyId);
+ assertEquals("Frozen column count should update when removing last row",
+ containerSize - 1, grid.getFrozenColumnCount());
+ }
+
+ @Test
+ public void testReorderColumns() {
+ Set<?> containerProperties = new LinkedHashSet<Object>(
+ grid.getContainerDataSource().getContainerPropertyIds());
+ Object[] properties = new Object[] { "column3", "column2", "column6" };
+ grid.setColumnOrder(properties);
+
+ int i = 0;
+ // Test sorted columns are first in order
+ for (Object property : properties) {
+ containerProperties.remove(property);
+ assertEquals(columnIdMapper.key(property),
+ state.columnOrder.get(i++));
+ }
+
+ // Test remaining columns are in original order
+ for (Object property : containerProperties) {
+ assertEquals(columnIdMapper.key(property),
+ state.columnOrder.get(i++));
+ }
+
+ try {
+ grid.setColumnOrder("foo", "bar", "baz");
+ fail("Grid allowed sorting with non-existent properties");
+ } catch (IllegalArgumentException e) {
+ // All ok
+ }
+ }
+
+ @Test(expected = IllegalArgumentException.class)
+ public void testRemoveColumnThatDoesNotExist() {
+ grid.removeColumn("banana phone");
+ }
+
+ @Test(expected = IllegalStateException.class)
+ public void testSetNonSortableColumnSortable() {
+ Column noSortColumn = grid.getColumn("noSort");
+ assertFalse("Object property column should not be sortable.",
+ noSortColumn.isSortable());
+ noSortColumn.setSortable(true);
+ }
+
+ @Test
+ public void testColumnsEditableByDefault() {
+ for (Column c : grid.getColumns()) {
+ assertTrue(c + " should be editable", c.isEditable());
+ }
+ }
+
+ @Test
+ public void testPropertyAndColumnEditorFieldsMatch() {
+ Column column1 = grid.getColumn("column1");
+ column1.setEditorField(new LegacyTextField());
+ assertSame(column1.getEditorField(),
+ grid.getColumn("column1").getEditorField());
+
+ Column column2 = grid.getColumn("column2");
+ column2.setEditorField(new LegacyTextField());
+ assertSame(column2.getEditorField(), column2.getEditorField());
+ }
+
+ @Test
+ public void testUneditableColumnHasNoField() {
+ Column col = grid.getColumn("column1");
+
+ col.setEditable(false);
+
+ assertFalse("Column should be uneditable", col.isEditable());
+ assertNull("Uneditable column should not be auto-assigned a Field",
+ col.getEditorField());
+ }
+
+ private GridColumnState getColumnState(Object propertyId) {
+ String columnId = columnIdMapper.key(propertyId);
+ for (GridColumnState columnState : state.columns) {
+ if (columnState.id.equals(columnId)) {
+ return columnState;
+ }
+ }
+ return null;
+ }
+
+ @Test
+ public void testAddAndRemoveSortableColumn() {
+ boolean sortable = grid.getColumn("column1").isSortable();
+ grid.removeColumn("column1");
+ grid.addColumn("column1");
+ assertEquals("Column sortability changed when re-adding", sortable,
+ grid.getColumn("column1").isSortable());
+ }
+
+ @Test
+ public void testSetColumns() {
+ grid.setColumns("column7", "column0", "column9");
+ Iterator<Column> it = grid.getColumns().iterator();
+ assertEquals(it.next().getPropertyId(), "column7");
+ assertEquals(it.next().getPropertyId(), "column0");
+ assertEquals(it.next().getPropertyId(), "column9");
+ assertFalse(it.hasNext());
+ }
+
+ @Test
+ public void testAddingColumnsWithSetColumns() {
+ LegacyGrid g = new LegacyGrid();
+ g.setColumns("c1", "c2", "c3");
+ Iterator<Column> it = g.getColumns().iterator();
+ assertEquals(it.next().getPropertyId(), "c1");
+ assertEquals(it.next().getPropertyId(), "c2");
+ assertEquals(it.next().getPropertyId(), "c3");
+ assertFalse(it.hasNext());
+ }
+
+ @Test(expected = IllegalStateException.class)
+ public void testAddingColumnsWithSetColumnsNonDefaultContainer() {
+ grid.setColumns("column1", "column2", "column50");
+ }
+
+ @Test
+ public void testDefaultColumnHidingToggleCaption() {
+ Column firstColumn = grid.getColumns().get(0);
+ firstColumn.setHeaderCaption("headerCaption");
+ assertEquals(null, firstColumn.getHidingToggleCaption());
+ }
+
+ @Test
+ public void testOverriddenColumnHidingToggleCaption() {
+ Column firstColumn = grid.getColumns().get(0);
+ firstColumn.setHidingToggleCaption("hidingToggleCaption");
+ firstColumn.setHeaderCaption("headerCaption");
+ assertEquals("hidingToggleCaption",
+ firstColumn.getHidingToggleCaption());
+ }
+
+ @Test
+ public void testColumnSetWidthFiresResizeEvent() {
+ final Column firstColumn = grid.getColumns().get(0);
+
+ // prepare a listener mock that captures the argument
+ ColumnResizeListener mock = EasyMock
+ .createMock(ColumnResizeListener.class);
+ Capture<ColumnResizeEvent> capturedEvent = new Capture<ColumnResizeEvent>();
+ mock.columnResize(
+ and(capture(capturedEvent), isA(ColumnResizeEvent.class)));
+ EasyMock.expectLastCall().once();
+
+ // Tell it to wait for the call
+ EasyMock.replay(mock);
+
+ // Cause a resize event
+ grid.addColumnResizeListener(mock);
+ firstColumn.setWidth(firstColumn.getWidth() + 10);
+
+ // Verify the method was called
+ EasyMock.verify(mock);
+
+ // Asserts on the captured event
+ ColumnResizeEvent event = capturedEvent.getValue();
+ assertEquals("Event column was not first column.", firstColumn,
+ event.getColumn());
+ assertFalse("Event should not be userOriginated",
+ event.isUserOriginated());
+ }
+
+ @Test
+ public void textHeaderCaptionIsReturned() {
+ Column firstColumn = grid.getColumns().get(0);
+
+ firstColumn.setHeaderCaption("text");
+
+ assertThat(firstColumn.getHeaderCaption(), is("text"));
+ }
+
+ @Test
+ public void defaultCaptionIsReturnedForHtml() {
+ Column firstColumn = grid.getColumns().get(0);
+
+ grid.getDefaultHeaderRow().getCell("column0").setHtml("<b>html</b>");
+
+ assertThat(firstColumn.getHeaderCaption(), is("Column0"));
+ }
+}
diff --git a/compatibility-server/src/test/java/com/vaadin/tests/server/component/grid/GridContainerNotSortableTest.java b/compatibility-server/src/test/java/com/vaadin/tests/server/component/grid/GridContainerNotSortableTest.java
new file mode 100644
index 0000000000..fa6c57df93
--- /dev/null
+++ b/compatibility-server/src/test/java/com/vaadin/tests/server/component/grid/GridContainerNotSortableTest.java
@@ -0,0 +1,103 @@
+/*
+ * Copyright 2000-2016 Vaadin Ltd.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not
+ * use this file except in compliance with the License. You may obtain a copy of
+ * the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+package com.vaadin.tests.server.component.grid;
+
+import static org.junit.Assert.assertFalse;
+
+import java.util.Collection;
+import java.util.LinkedHashMap;
+import java.util.Map;
+
+import com.vaadin.ui.LegacyGrid;
+import org.junit.Test;
+
+import com.vaadin.data.Item;
+import com.vaadin.data.Property;
+import com.vaadin.data.util.AbstractInMemoryContainer;
+import com.vaadin.ui.LegacyGrid.Column;
+
+public class GridContainerNotSortableTest {
+
+ final AbstractInMemoryContainer<Object, Object, Item> notSortableDataSource = new AbstractInMemoryContainer<Object, Object, Item>() {
+
+ private Map<Object, Property<?>> properties = new LinkedHashMap<Object, Property<?>>();
+
+ {
+ properties.put("Foo", new Property<String>() {
+
+ @Override
+ public String getValue() {
+ return "foo";
+ }
+
+ @Override
+ public void setValue(String newValue) throws ReadOnlyException {
+ throw new ReadOnlyException();
+ }
+
+ @Override
+ public Class<? extends String> getType() {
+ return String.class;
+ }
+
+ @Override
+ public boolean isReadOnly() {
+ return true;
+ }
+
+ @Override
+ public void setReadOnly(boolean newStatus) {
+ throw new UnsupportedOperationException();
+ }
+ });
+ }
+
+ @Override
+ public Collection<?> getContainerPropertyIds() {
+ return properties.keySet();
+ }
+
+ @Override
+ public Property getContainerProperty(Object itemId, Object propertyId) {
+ return properties.get(propertyId);
+ }
+
+ @Override
+ public Class<?> getType(Object propertyId) {
+ return properties.get(propertyId).getType();
+ }
+
+ @Override
+ protected Item getUnfilteredItem(Object itemId) {
+ return null;
+ }
+ };
+
+ @Test
+ public void testGridWithNotSortableContainer() {
+ new LegacyGrid(notSortableDataSource);
+ }
+
+ @Test(expected = IllegalStateException.class)
+ public void testNotSortableGridSetColumnSortable() {
+ LegacyGrid grid = new LegacyGrid();
+ grid.setContainerDataSource(notSortableDataSource);
+ Column column = grid.getColumn("Foo");
+ assertFalse("Column should not be sortable initially.",
+ column.isSortable());
+ column.setSortable(true);
+ }
+}
diff --git a/compatibility-server/src/test/java/com/vaadin/tests/server/component/grid/GridContainerTest.java b/compatibility-server/src/test/java/com/vaadin/tests/server/component/grid/GridContainerTest.java
new file mode 100644
index 0000000000..7b07aefcb2
--- /dev/null
+++ b/compatibility-server/src/test/java/com/vaadin/tests/server/component/grid/GridContainerTest.java
@@ -0,0 +1,159 @@
+/*
+ * Copyright 2000-2016 Vaadin Ltd.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not
+ * use this file except in compliance with the License. You may obtain a copy of
+ * the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+package com.vaadin.tests.server.component.grid;
+
+import java.io.IOException;
+import java.io.ObjectOutputStream;
+import java.io.OutputStream;
+
+import com.vaadin.ui.LegacyGrid;
+import org.junit.Assert;
+import org.junit.Test;
+
+import com.vaadin.data.util.IndexedContainer;
+import com.vaadin.ui.Component;
+import com.vaadin.ui.LegacyGrid.DetailsGenerator;
+import com.vaadin.ui.LegacyGrid.RowReference;
+import com.vaadin.ui.Label;
+
+public class GridContainerTest {
+
+ /**
+ * Null Stream used with serialization tests
+ */
+ protected static OutputStream NULLSTREAM = new OutputStream() {
+ @Override
+ public void write(int b) {
+ }
+ };
+
+ @Test
+ public void testDetailsGeneratorDoesNotResetOnContainerChange() {
+ LegacyGrid grid = new LegacyGrid();
+ DetailsGenerator detGen = new DetailsGenerator() {
+
+ @Override
+ public Component getDetails(RowReference rowReference) {
+ return new Label("Empty details");
+ }
+ };
+ grid.setDetailsGenerator(detGen);
+
+ grid.setContainerDataSource(createContainer());
+
+ Assert.assertEquals("DetailsGenerator changed", detGen,
+ grid.getDetailsGenerator());
+ }
+
+ @Test
+ public void testSetContainerTwice() throws Exception {
+
+ TestGrid grid = new TestGrid();
+
+ grid.setContainerDataSource(createContainer());
+
+ // Simulate initial response to ensure "lazy" state changes are done
+ // before resetting the datasource
+ grid.beforeClientResponse(true);
+ grid.getDataProvider().beforeClientResponse(true);
+
+ grid.setContainerDataSource(createContainer());
+ }
+
+ @SuppressWarnings("unchecked")
+ private IndexedContainer createContainer() {
+ IndexedContainer container = new IndexedContainer();
+ container.addContainerProperty("x", String.class, null);
+ container.addItem(0).getItemProperty("x").setValue("y");
+ return container;
+ }
+
+ @Test
+ public void setColumnsOrder() {
+ LegacyGrid grid = new LegacyGrid();
+ IndexedContainer ic = new IndexedContainer();
+ ic.addContainerProperty("foo", String.class, "");
+ ic.addContainerProperty("baz", String.class, "");
+ ic.addContainerProperty("bar", String.class, "");
+ grid.setContainerDataSource(ic);
+ grid.setColumns("foo", "baz", "bar");
+
+ Assert.assertEquals("foo", grid.getColumns().get(0).getPropertyId());
+ Assert.assertEquals("baz", grid.getColumns().get(1).getPropertyId());
+ Assert.assertEquals("bar", grid.getColumns().get(2).getPropertyId());
+ }
+
+ @Test
+ public void addColumnNotInContainer() {
+ LegacyGrid grid = new LegacyGrid();
+ grid.setContainerDataSource(new IndexedContainer());
+ try {
+ grid.addColumn("notInContainer");
+ Assert.fail(
+ "Adding a property id not in the container should throw an exception");
+ } catch (IllegalStateException e) {
+ Assert.assertTrue(e.getMessage().contains("notInContainer"));
+ Assert.assertTrue(
+ e.getMessage().contains("does not exist in the container"));
+ }
+ }
+
+ @Test
+ public void setColumnsForPropertyIdNotInContainer() {
+ LegacyGrid grid = new LegacyGrid();
+ grid.setContainerDataSource(new IndexedContainer());
+ try {
+ grid.setColumns("notInContainer", "notThereEither");
+ Assert.fail(
+ "Setting columns for property ids not in the container should throw an exception");
+ } catch (IllegalStateException e) {
+ // addColumn is run in random order..
+ Assert.assertTrue(e.getMessage().contains("notInContainer")
+ || e.getMessage().contains("notThereEither"));
+ Assert.assertTrue(
+ e.getMessage().contains("does not exist in the container"));
+ }
+ }
+
+ @Test(expected = IllegalStateException.class)
+ public void multipleAddColumnsForDefaultContainer() {
+ LegacyGrid grid = new LegacyGrid();
+ grid.addColumn("foo");
+ grid.addColumn("foo");
+ }
+
+ @Test
+ public void testSerializeRpcDataProviderWithRowChanges()
+ throws IOException {
+ LegacyGrid grid = new LegacyGrid();
+ IndexedContainer container = new IndexedContainer();
+ grid.setContainerDataSource(container);
+ container.addItem();
+ serializeComponent(grid);
+ }
+
+ protected void serializeComponent(Component component) throws IOException {
+ ObjectOutputStream stream = null;
+ try {
+ stream = new ObjectOutputStream(NULLSTREAM);
+ stream.writeObject(component);
+ } finally {
+ if (stream != null) {
+ stream.close();
+ }
+ }
+ }
+}
diff --git a/compatibility-server/src/test/java/com/vaadin/tests/server/component/grid/GridEditorTest.java b/compatibility-server/src/test/java/com/vaadin/tests/server/component/grid/GridEditorTest.java
new file mode 100644
index 0000000000..15daa264d9
--- /dev/null
+++ b/compatibility-server/src/test/java/com/vaadin/tests/server/component/grid/GridEditorTest.java
@@ -0,0 +1,296 @@
+/*
+ * Copyright 2000-2016 Vaadin Ltd.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not
+ * use this file except in compliance with the License. You may obtain a copy of
+ * the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+package com.vaadin.tests.server.component.grid;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertSame;
+import static org.junit.Assert.assertTrue;
+
+import java.lang.reflect.Method;
+
+import com.vaadin.ui.LegacyGrid;
+import com.vaadin.v7.ui.LegacyField;
+import com.vaadin.v7.ui.LegacyTextField;
+
+import org.easymock.EasyMock;
+import org.junit.After;
+import org.junit.Assert;
+import org.junit.Before;
+import org.junit.Test;
+
+import com.vaadin.data.Item;
+import com.vaadin.data.Property;
+import com.vaadin.data.fieldgroup.FieldGroup;
+import com.vaadin.data.fieldgroup.FieldGroup.CommitException;
+import com.vaadin.data.util.IndexedContainer;
+import com.vaadin.server.MockVaadinSession;
+import com.vaadin.server.VaadinService;
+import com.vaadin.server.VaadinSession;
+
+public class GridEditorTest {
+
+ private static final Object PROPERTY_NAME = "name";
+ private static final Object PROPERTY_AGE = "age";
+ private static final String DEFAULT_NAME = "Some Valid Name";
+ private static final Integer DEFAULT_AGE = 25;
+ private static final Object ITEM_ID = new Object();
+
+ // Explicit field for the test session to save it from GC
+ private VaadinSession session;
+
+ private final LegacyGrid grid = new LegacyGrid();
+ private Method doEditMethod;
+
+ @Before
+ @SuppressWarnings("unchecked")
+ public void setup() throws SecurityException, NoSuchMethodException {
+ IndexedContainer container = new IndexedContainer();
+ container.addContainerProperty(PROPERTY_NAME, String.class, "[name]");
+ container.addContainerProperty(PROPERTY_AGE, Integer.class,
+ Integer.valueOf(-1));
+
+ Item item = container.addItem(ITEM_ID);
+ item.getItemProperty(PROPERTY_NAME).setValue(DEFAULT_NAME);
+ item.getItemProperty(PROPERTY_AGE).setValue(DEFAULT_AGE);
+ grid.setContainerDataSource(container);
+
+ // VaadinSession needed for ConverterFactory
+ VaadinService mockService = EasyMock
+ .createNiceMock(VaadinService.class);
+ session = new MockVaadinSession(mockService);
+ VaadinSession.setCurrent(session);
+ session.lock();
+
+ // Access to method for actual editing.
+ doEditMethod = LegacyGrid.class.getDeclaredMethod("doEditItem");
+ doEditMethod.setAccessible(true);
+ }
+
+ @After
+ public void tearDown() {
+ session.unlock();
+ session = null;
+ VaadinSession.setCurrent(null);
+ }
+
+ @Test
+ public void testInitAssumptions() throws Exception {
+ assertFalse(grid.isEditorEnabled());
+ assertNull(grid.getEditedItemId());
+ assertNotNull(grid.getEditorFieldGroup());
+ }
+
+ @Test
+ public void testSetEnabled() throws Exception {
+ assertFalse(grid.isEditorEnabled());
+ grid.setEditorEnabled(true);
+ assertTrue(grid.isEditorEnabled());
+ }
+
+ @Test
+ public void testSetDisabled() throws Exception {
+ assertFalse(grid.isEditorEnabled());
+ grid.setEditorEnabled(true);
+ grid.setEditorEnabled(false);
+ assertFalse(grid.isEditorEnabled());
+ }
+
+ @Test
+ public void testSetReEnabled() throws Exception {
+ assertFalse(grid.isEditorEnabled());
+ grid.setEditorEnabled(true);
+ grid.setEditorEnabled(false);
+ grid.setEditorEnabled(true);
+ assertTrue(grid.isEditorEnabled());
+ }
+
+ @Test
+ public void testDetached() throws Exception {
+ FieldGroup oldFieldGroup = grid.getEditorFieldGroup();
+ grid.removeAllColumns();
+ grid.setContainerDataSource(new IndexedContainer());
+ assertFalse(oldFieldGroup == grid.getEditorFieldGroup());
+ }
+
+ @Test(expected = IllegalStateException.class)
+ public void testDisabledEditItem() throws Exception {
+ grid.editItem(ITEM_ID);
+ }
+
+ @Test
+ public void testEditItem() throws Exception {
+ startEdit();
+ assertEquals(ITEM_ID, grid.getEditedItemId());
+ assertEquals(getEditedItem(),
+ grid.getEditorFieldGroup().getItemDataSource());
+
+ assertEquals(DEFAULT_NAME,
+ grid.getColumn(PROPERTY_NAME).getEditorField().getValue());
+ assertEquals(String.valueOf(DEFAULT_AGE),
+ grid.getColumn(PROPERTY_AGE).getEditorField().getValue());
+ }
+
+ @Test
+ public void testSaveEditor() throws Exception {
+ startEdit();
+ LegacyTextField field = (LegacyTextField) grid.getColumn(PROPERTY_NAME)
+ .getEditorField();
+
+ field.setValue("New Name");
+ assertEquals(DEFAULT_NAME, field.getPropertyDataSource().getValue());
+
+ grid.saveEditor();
+ assertTrue(grid.isEditorActive());
+ assertFalse(field.isModified());
+ assertEquals("New Name", field.getValue());
+ assertEquals("New Name", getEditedProperty(PROPERTY_NAME).getValue());
+ }
+
+ @Test
+ public void testSaveEditorCommitFail() throws Exception {
+ startEdit();
+
+ ((LegacyTextField) grid.getColumn(PROPERTY_AGE).getEditorField())
+ .setValue("Invalid");
+ try {
+ // Manual fail instead of @Test(expected=...) to check it is
+ // saveEditor that fails and not setValue
+ grid.saveEditor();
+ Assert.fail(
+ "CommitException expected when saving an invalid field value");
+ } catch (CommitException e) {
+ // expected
+ }
+ }
+
+ @Test
+ public void testCancelEditor() throws Exception {
+ startEdit();
+ LegacyTextField field = (LegacyTextField) grid.getColumn(PROPERTY_NAME)
+ .getEditorField();
+ field.setValue("New Name");
+
+ Property<?> datasource = field.getPropertyDataSource();
+
+ grid.cancelEditor();
+ assertFalse(grid.isEditorActive());
+ assertNull(grid.getEditedItemId());
+ assertFalse(field.isModified());
+ assertEquals("", field.getValue());
+ assertEquals(DEFAULT_NAME, datasource.getValue());
+ assertNull(field.getPropertyDataSource());
+ assertNull(grid.getEditorFieldGroup().getItemDataSource());
+ }
+
+ @Test(expected = IllegalArgumentException.class)
+ public void testNonexistentEditItem() throws Exception {
+ grid.setEditorEnabled(true);
+ grid.editItem(new Object());
+ }
+
+ @Test
+ public void testGetField() throws Exception {
+ startEdit();
+
+ assertNotNull(grid.getColumn(PROPERTY_NAME).getEditorField());
+ }
+
+ @Test
+ public void testGetFieldWithoutItem() throws Exception {
+ grid.setEditorEnabled(true);
+ assertNotNull(grid.getColumn(PROPERTY_NAME).getEditorField());
+ }
+
+ @Test
+ public void testCustomBinding() {
+ LegacyTextField textField = new LegacyTextField();
+ grid.getColumn(PROPERTY_NAME).setEditorField(textField);
+
+ startEdit();
+
+ assertSame(textField, grid.getColumn(PROPERTY_NAME).getEditorField());
+ }
+
+ @Test(expected = IllegalStateException.class)
+ public void testDisableWhileEditing() {
+ startEdit();
+ grid.setEditorEnabled(false);
+ }
+
+ @Test
+ public void testFieldIsNotReadonly() {
+ startEdit();
+
+ LegacyField<?> field = grid.getColumn(PROPERTY_NAME).getEditorField();
+ assertFalse(field.isReadOnly());
+ }
+
+ @Test
+ public void testFieldIsReadonlyWhenFieldGroupIsReadonly() {
+ startEdit();
+
+ grid.getEditorFieldGroup().setReadOnly(true);
+ LegacyField<?> field = grid.getColumn(PROPERTY_NAME).getEditorField();
+ assertTrue(field.isReadOnly());
+ }
+
+ @Test
+ public void testColumnRemoved() {
+ LegacyField<?> field = grid.getColumn(PROPERTY_NAME).getEditorField();
+
+ assertSame("field should be attached to ", grid, field.getParent());
+
+ grid.removeColumn(PROPERTY_NAME);
+
+ assertNull("field should be detached from ", field.getParent());
+ }
+
+ @Test
+ public void testSetFieldAgain() {
+ LegacyTextField field = new LegacyTextField();
+ grid.getColumn(PROPERTY_NAME).setEditorField(field);
+
+ field = new LegacyTextField();
+ grid.getColumn(PROPERTY_NAME).setEditorField(field);
+
+ assertSame("new field should be used.", field,
+ grid.getColumn(PROPERTY_NAME).getEditorField());
+ }
+
+ private void startEdit() {
+ grid.setEditorEnabled(true);
+ grid.editItem(ITEM_ID);
+ // Simulate succesful client response to actually start the editing.
+ try {
+ doEditMethod.invoke(grid);
+ } catch (Exception e) {
+ Assert.fail("Editing item " + ITEM_ID + " failed. Cause: "
+ + e.getCause().toString());
+ }
+ }
+
+ private Item getEditedItem() {
+ assertNotNull(grid.getEditedItemId());
+ return grid.getContainerDataSource().getItem(grid.getEditedItemId());
+ }
+
+ private Property<?> getEditedProperty(Object propertyId) {
+ return getEditedItem().getItemProperty(PROPERTY_NAME);
+ }
+}
diff --git a/compatibility-server/src/test/java/com/vaadin/tests/server/component/grid/GridExtensionTest.java b/compatibility-server/src/test/java/com/vaadin/tests/server/component/grid/GridExtensionTest.java
new file mode 100644
index 0000000000..452d2713a4
--- /dev/null
+++ b/compatibility-server/src/test/java/com/vaadin/tests/server/component/grid/GridExtensionTest.java
@@ -0,0 +1,41 @@
+/*
+ * Copyright 2000-2016 Vaadin Ltd.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not
+ * use this file except in compliance with the License. You may obtain a copy of
+ * the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+package com.vaadin.tests.server.component.grid;
+
+import static org.junit.Assert.assertTrue;
+
+import com.vaadin.ui.LegacyGrid;
+import org.junit.Test;
+
+import com.vaadin.ui.LegacyGrid.AbstractGridExtension;
+
+public class GridExtensionTest {
+
+ public static class DummyGridExtension extends AbstractGridExtension {
+
+ public DummyGridExtension(LegacyGrid grid) {
+ super(grid);
+ }
+ }
+
+ @Test
+ public void testCreateExtension() {
+ LegacyGrid grid = new LegacyGrid();
+ DummyGridExtension dummy = new DummyGridExtension(grid);
+ assertTrue("DummyGridExtension never made it to Grid",
+ grid.getExtensions().contains(dummy));
+ }
+}
diff --git a/compatibility-server/src/test/java/com/vaadin/tests/server/component/grid/GridSelectionTest.java b/compatibility-server/src/test/java/com/vaadin/tests/server/component/grid/GridSelectionTest.java
new file mode 100644
index 0000000000..7e910d2428
--- /dev/null
+++ b/compatibility-server/src/test/java/com/vaadin/tests/server/component/grid/GridSelectionTest.java
@@ -0,0 +1,377 @@
+/*
+ * Copyright 2000-2013 Vaadin Ltd.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not
+ * use this file except in compliance with the License. You may obtain a copy of
+ * the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+package com.vaadin.tests.server.component.grid;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
+
+import java.util.Collection;
+
+import com.vaadin.ui.LegacyGrid;
+import org.junit.Before;
+import org.junit.Test;
+
+import com.vaadin.data.util.IndexedContainer;
+import com.vaadin.event.SelectionEvent;
+import com.vaadin.event.SelectionEvent.SelectionListener;
+import com.vaadin.ui.LegacyGrid.SelectionMode;
+import com.vaadin.ui.LegacyGrid.SelectionModel;
+
+public class GridSelectionTest {
+
+ private static class MockSelectionChangeListener
+ implements SelectionListener {
+ private SelectionEvent event;
+
+ @Override
+ public void select(final SelectionEvent event) {
+ this.event = event;
+ }
+
+ public Collection<?> getAdded() {
+ return event.getAdded();
+ }
+
+ public Collection<?> getRemoved() {
+ return event.getRemoved();
+ }
+
+ public void clearEvent() {
+ /*
+ * This method is not strictly needed as the event will simply be
+ * overridden, but it's good practice, and makes the code more
+ * obvious.
+ */
+ event = null;
+ }
+
+ public boolean eventHasHappened() {
+ return event != null;
+ }
+ }
+
+ private LegacyGrid grid;
+ private MockSelectionChangeListener mockListener;
+
+ private final Object itemId1Present = "itemId1Present";
+ private final Object itemId2Present = "itemId2Present";
+
+ private final Object itemId1NotPresent = "itemId1NotPresent";
+ private final Object itemId2NotPresent = "itemId2NotPresent";
+
+ @Before
+ public void setup() {
+ final IndexedContainer container = new IndexedContainer();
+ container.addItem(itemId1Present);
+ container.addItem(itemId2Present);
+ for (int i = 2; i < 10; i++) {
+ container.addItem(new Object());
+ }
+
+ assertEquals("init size", 10, container.size());
+ assertTrue("itemId1Present", container.containsId(itemId1Present));
+ assertTrue("itemId2Present", container.containsId(itemId2Present));
+ assertFalse("itemId1NotPresent",
+ container.containsId(itemId1NotPresent));
+ assertFalse("itemId2NotPresent",
+ container.containsId(itemId2NotPresent));
+
+ grid = new LegacyGrid(container);
+
+ mockListener = new MockSelectionChangeListener();
+ grid.addSelectionListener(mockListener);
+
+ assertFalse("eventHasHappened", mockListener.eventHasHappened());
+ }
+
+ @Test
+ public void defaultSelectionModeIsSingle() {
+ assertTrue(grid.getSelectionModel() instanceof SelectionModel.Single);
+ }
+
+ @Test(expected = IllegalStateException.class)
+ public void getSelectedRowThrowsExceptionMulti() {
+ grid.setSelectionMode(SelectionMode.MULTI);
+ grid.getSelectedRow();
+ }
+
+ @Test(expected = IllegalStateException.class)
+ public void getSelectedRowThrowsExceptionNone() {
+ grid.setSelectionMode(SelectionMode.NONE);
+ grid.getSelectedRow();
+ }
+
+ @Test(expected = IllegalStateException.class)
+ public void selectThrowsExceptionNone() {
+ grid.setSelectionMode(SelectionMode.NONE);
+ grid.select(itemId1Present);
+ }
+
+ @Test(expected = IllegalStateException.class)
+ public void deselectRowThrowsExceptionNone() {
+ grid.setSelectionMode(SelectionMode.NONE);
+ grid.deselect(itemId1Present);
+ }
+
+ @Test
+ public void selectionModeMapsToMulti() {
+ assertTrue(grid.setSelectionMode(
+ SelectionMode.MULTI) instanceof SelectionModel.Multi);
+ }
+
+ @Test
+ public void selectionModeMapsToSingle() {
+ assertTrue(grid.setSelectionMode(
+ SelectionMode.SINGLE) instanceof SelectionModel.Single);
+ }
+
+ @Test
+ public void selectionModeMapsToNone() {
+ assertTrue(grid.setSelectionMode(
+ SelectionMode.NONE) instanceof SelectionModel.None);
+ }
+
+ @Test(expected = IllegalArgumentException.class)
+ public void selectionModeNullThrowsException() {
+ grid.setSelectionMode(null);
+ }
+
+ @Test
+ public void noSelectModel_isSelected() {
+ grid.setSelectionMode(SelectionMode.NONE);
+ assertFalse("itemId1Present", grid.isSelected(itemId1Present));
+ assertFalse("itemId1NotPresent", grid.isSelected(itemId1NotPresent));
+ }
+
+ @Test(expected = IllegalStateException.class)
+ public void noSelectModel_getSelectedRow() {
+ grid.setSelectionMode(SelectionMode.NONE);
+ grid.getSelectedRow();
+ }
+
+ @Test
+ public void noSelectModel_getSelectedRows() {
+ grid.setSelectionMode(SelectionMode.NONE);
+ assertTrue(grid.getSelectedRows().isEmpty());
+ }
+
+ @Test
+ public void selectionCallsListenerMulti() {
+ grid.setSelectionMode(SelectionMode.MULTI);
+ selectionCallsListener();
+ }
+
+ @Test
+ public void selectionCallsListenerSingle() {
+ grid.setSelectionMode(SelectionMode.SINGLE);
+ selectionCallsListener();
+ }
+
+ private void selectionCallsListener() {
+ grid.select(itemId1Present);
+ assertEquals("added size", 1, mockListener.getAdded().size());
+ assertEquals("added item", itemId1Present,
+ mockListener.getAdded().iterator().next());
+ assertEquals("removed size", 0, mockListener.getRemoved().size());
+ }
+
+ @Test
+ public void deselectionCallsListenerMulti() {
+ grid.setSelectionMode(SelectionMode.MULTI);
+ deselectionCallsListener();
+ }
+
+ @Test
+ public void deselectionCallsListenerSingle() {
+ grid.setSelectionMode(SelectionMode.SINGLE);
+ deselectionCallsListener();
+ }
+
+ private void deselectionCallsListener() {
+ grid.select(itemId1Present);
+ mockListener.clearEvent();
+
+ grid.deselect(itemId1Present);
+ assertEquals("removed size", 1, mockListener.getRemoved().size());
+ assertEquals("removed item", itemId1Present,
+ mockListener.getRemoved().iterator().next());
+ assertEquals("removed size", 0, mockListener.getAdded().size());
+ }
+
+ @Test
+ public void deselectPresentButNotSelectedItemIdShouldntFireListenerMulti() {
+ grid.setSelectionMode(SelectionMode.MULTI);
+ deselectPresentButNotSelectedItemIdShouldntFireListener();
+ }
+
+ @Test
+ public void deselectPresentButNotSelectedItemIdShouldntFireListenerSingle() {
+ grid.setSelectionMode(SelectionMode.SINGLE);
+ deselectPresentButNotSelectedItemIdShouldntFireListener();
+ }
+
+ private void deselectPresentButNotSelectedItemIdShouldntFireListener() {
+ grid.deselect(itemId1Present);
+ assertFalse(mockListener.eventHasHappened());
+ }
+
+ @Test
+ public void deselectNotPresentItemIdShouldNotThrowExceptionMulti() {
+ grid.setSelectionMode(SelectionMode.MULTI);
+ grid.deselect(itemId1NotPresent);
+ }
+
+ @Test
+ public void deselectNotPresentItemIdShouldNotThrowExceptionSingle() {
+ grid.setSelectionMode(SelectionMode.SINGLE);
+ grid.deselect(itemId1NotPresent);
+ }
+
+ @Test(expected = IllegalArgumentException.class)
+ public void selectNotPresentItemIdShouldThrowExceptionMulti() {
+ grid.setSelectionMode(SelectionMode.MULTI);
+ grid.select(itemId1NotPresent);
+ }
+
+ @Test(expected = IllegalArgumentException.class)
+ public void selectNotPresentItemIdShouldThrowExceptionSingle() {
+ grid.setSelectionMode(SelectionMode.SINGLE);
+ grid.select(itemId1NotPresent);
+ }
+
+ @Test
+ public void selectAllMulti() {
+ grid.setSelectionMode(SelectionMode.MULTI);
+ final SelectionModel.Multi select = (SelectionModel.Multi) grid
+ .getSelectionModel();
+ select.selectAll();
+ assertEquals("added size", 10, mockListener.getAdded().size());
+ assertEquals("removed size", 0, mockListener.getRemoved().size());
+ assertTrue("itemId1Present",
+ mockListener.getAdded().contains(itemId1Present));
+ assertTrue("itemId2Present",
+ mockListener.getAdded().contains(itemId2Present));
+ }
+
+ @Test
+ public void deselectAllMulti() {
+ grid.setSelectionMode(SelectionMode.MULTI);
+ final SelectionModel.Multi select = (SelectionModel.Multi) grid
+ .getSelectionModel();
+ select.selectAll();
+ mockListener.clearEvent();
+
+ select.deselectAll();
+ assertEquals("removed size", 10, mockListener.getRemoved().size());
+ assertEquals("added size", 0, mockListener.getAdded().size());
+ assertTrue("itemId1Present",
+ mockListener.getRemoved().contains(itemId1Present));
+ assertTrue("itemId2Present",
+ mockListener.getRemoved().contains(itemId2Present));
+ assertTrue("selectedRows is empty", grid.getSelectedRows().isEmpty());
+ }
+
+ @Test
+ public void gridDeselectAllMultiAllSelected() {
+ grid.setSelectionMode(SelectionMode.MULTI);
+ final SelectionModel.Multi select = (SelectionModel.Multi) grid
+ .getSelectionModel();
+ select.selectAll();
+ mockListener.clearEvent();
+
+ assertTrue(grid.deselectAll());
+ assertEquals("removed size", 10, mockListener.getRemoved().size());
+ assertEquals("added size", 0, mockListener.getAdded().size());
+ assertTrue("itemId1Present",
+ mockListener.getRemoved().contains(itemId1Present));
+ assertTrue("itemId2Present",
+ mockListener.getRemoved().contains(itemId2Present));
+ assertTrue("selectedRows is empty", grid.getSelectedRows().isEmpty());
+
+ }
+
+ @Test
+ public void gridDeselectAllMultiOneSelected() {
+ grid.setSelectionMode(SelectionMode.MULTI);
+ final SelectionModel.Multi select = (SelectionModel.Multi) grid
+ .getSelectionModel();
+ select.select(itemId2Present);
+ mockListener.clearEvent();
+
+ assertTrue(grid.deselectAll());
+ assertEquals("removed size", 1, mockListener.getRemoved().size());
+ assertEquals("added size", 0, mockListener.getAdded().size());
+ assertFalse("itemId1Present",
+ mockListener.getRemoved().contains(itemId1Present));
+ assertTrue("itemId2Present",
+ mockListener.getRemoved().contains(itemId2Present));
+ assertTrue("selectedRows is empty", grid.getSelectedRows().isEmpty());
+
+ }
+
+ @Test
+ public void gridDeselectAllSingleNoneSelected() {
+ grid.setSelectionMode(SelectionMode.SINGLE);
+ assertFalse(grid.deselectAll());
+ assertTrue("selectedRows is empty", grid.getSelectedRows().isEmpty());
+ }
+
+ @Test
+ public void gridDeselectAllSingleOneSelected() {
+ grid.setSelectionMode(SelectionMode.SINGLE);
+ final SelectionModel.Single select = (SelectionModel.Single) grid
+ .getSelectionModel();
+ select.select(itemId2Present);
+ mockListener.clearEvent();
+
+ assertTrue(grid.deselectAll());
+ assertEquals("removed size", 1, mockListener.getRemoved().size());
+ assertEquals("added size", 0, mockListener.getAdded().size());
+ assertFalse("itemId1Present",
+ mockListener.getRemoved().contains(itemId1Present));
+ assertTrue("itemId2Present",
+ mockListener.getRemoved().contains(itemId2Present));
+ assertTrue("selectedRows is empty", grid.getSelectedRows().isEmpty());
+
+ }
+
+ @Test
+ public void gridDeselectAllMultiNoneSelected() {
+ grid.setSelectionMode(SelectionMode.MULTI);
+
+ assertFalse(grid.deselectAll());
+ assertTrue("selectedRows is empty", grid.getSelectedRows().isEmpty());
+
+ }
+
+ @Test
+ public void reselectionDeselectsPreviousSingle() {
+ grid.setSelectionMode(SelectionMode.SINGLE);
+ grid.select(itemId1Present);
+ mockListener.clearEvent();
+
+ grid.select(itemId2Present);
+ assertEquals("added size", 1, mockListener.getAdded().size());
+ assertEquals("removed size", 1, mockListener.getRemoved().size());
+ assertEquals("added item", itemId2Present,
+ mockListener.getAdded().iterator().next());
+ assertEquals("removed item", itemId1Present,
+ mockListener.getRemoved().iterator().next());
+ assertEquals("selectedRows is correct", itemId2Present,
+ grid.getSelectedRow());
+ }
+}
diff --git a/compatibility-server/src/test/java/com/vaadin/tests/server/component/grid/GridStateTest.java b/compatibility-server/src/test/java/com/vaadin/tests/server/component/grid/GridStateTest.java
new file mode 100644
index 0000000000..9f5f67d8be
--- /dev/null
+++ b/compatibility-server/src/test/java/com/vaadin/tests/server/component/grid/GridStateTest.java
@@ -0,0 +1,44 @@
+/*
+ * Copyright 2000-2016 Vaadin Ltd.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not
+ * use this file except in compliance with the License. You may obtain a copy of
+ * the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+package com.vaadin.tests.server.component.grid;
+
+import org.junit.Assert;
+import org.junit.Test;
+
+import com.vaadin.shared.ui.grid.GridState;
+import com.vaadin.ui.LegacyGrid;
+
+/**
+ * Tests for Grid State.
+ *
+ */
+public class GridStateTest {
+
+ @Test
+ public void getPrimaryStyleName_gridHasCustomPrimaryStyleName() {
+ LegacyGrid grid = new LegacyGrid();
+ GridState state = new GridState();
+ Assert.assertEquals("Unexpected primary style name",
+ state.primaryStyleName, grid.getPrimaryStyleName());
+ }
+
+ @Test
+ public void gridStateHasCustomPrimaryStyleName() {
+ GridState state = new GridState();
+ Assert.assertEquals("Unexpected primary style name", "v-grid",
+ state.primaryStyleName);
+ }
+}
diff --git a/compatibility-server/src/test/java/com/vaadin/tests/server/component/grid/GridStaticSectionTest.java b/compatibility-server/src/test/java/com/vaadin/tests/server/component/grid/GridStaticSectionTest.java
new file mode 100644
index 0000000000..7a8209d50b
--- /dev/null
+++ b/compatibility-server/src/test/java/com/vaadin/tests/server/component/grid/GridStaticSectionTest.java
@@ -0,0 +1,132 @@
+/*
+ * Copyright 2000-2016 Vaadin Ltd.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not
+ * use this file except in compliance with the License. You may obtain a copy of
+ * the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+package com.vaadin.tests.server.component.grid;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertNull;
+
+import java.lang.reflect.Method;
+
+import com.vaadin.ui.LegacyGrid;
+import org.junit.Before;
+import org.junit.Test;
+
+import com.vaadin.data.Container.Indexed;
+import com.vaadin.data.util.IndexedContainer;
+
+public class GridStaticSectionTest extends LegacyGrid {
+
+ private Indexed dataSource = new IndexedContainer();
+
+ @Before
+ public void setUp() {
+ dataSource.addContainerProperty("firstName", String.class, "");
+ dataSource.addContainerProperty("lastName", String.class, "");
+ dataSource.addContainerProperty("streetAddress", String.class, "");
+ dataSource.addContainerProperty("zipCode", Integer.class, null);
+ setContainerDataSource(dataSource);
+ }
+
+ @Test
+ public void testAddAndRemoveHeaders() {
+ assertEquals(1, getHeaderRowCount());
+ prependHeaderRow();
+ assertEquals(2, getHeaderRowCount());
+ removeHeaderRow(0);
+ assertEquals(1, getHeaderRowCount());
+ removeHeaderRow(0);
+ assertEquals(0, getHeaderRowCount());
+ assertEquals(null, getDefaultHeaderRow());
+ HeaderRow row = appendHeaderRow();
+ assertEquals(1, getHeaderRowCount());
+ assertEquals(null, getDefaultHeaderRow());
+ setDefaultHeaderRow(row);
+ assertEquals(row, getDefaultHeaderRow());
+ }
+
+ @Test
+ public void testAddAndRemoveFooters() {
+ // By default there are no footer rows
+ assertEquals(0, getFooterRowCount());
+ FooterRow row = appendFooterRow();
+
+ assertEquals(1, getFooterRowCount());
+ prependFooterRow();
+ assertEquals(2, getFooterRowCount());
+ assertEquals(row, getFooterRow(1));
+ removeFooterRow(0);
+ assertEquals(1, getFooterRowCount());
+ removeFooterRow(0);
+ assertEquals(0, getFooterRowCount());
+ }
+
+ @Test
+ public void testUnusedPropertyNotInCells() {
+ removeColumn("firstName");
+ assertNull("firstName cell was not removed from existing row",
+ getDefaultHeaderRow().getCell("firstName"));
+ HeaderRow newRow = appendHeaderRow();
+ assertNull("firstName cell was created when it should not.",
+ newRow.getCell("firstName"));
+ addColumn("firstName");
+ assertNotNull(
+ "firstName cell was not created for default row when added again",
+ getDefaultHeaderRow().getCell("firstName"));
+ assertNotNull(
+ "firstName cell was not created for new row when added again",
+ newRow.getCell("firstName"));
+
+ }
+
+ @Test
+ public void testJoinHeaderCells() {
+ HeaderRow mergeRow = prependHeaderRow();
+ mergeRow.join("firstName", "lastName").setText("Name");
+ mergeRow.join(mergeRow.getCell("streetAddress"),
+ mergeRow.getCell("zipCode"));
+ }
+
+ @Test(expected = IllegalStateException.class)
+ public void testJoinHeaderCellsIncorrectly() throws Throwable {
+ HeaderRow mergeRow = prependHeaderRow();
+ mergeRow.join("firstName", "zipCode").setText("Name");
+ sanityCheck();
+ }
+
+ @Test
+ public void testJoinAllFooterCells() {
+ FooterRow mergeRow = prependFooterRow();
+ mergeRow.join(dataSource.getContainerPropertyIds().toArray())
+ .setText("All the stuff.");
+ }
+
+ private void sanityCheck() throws Throwable {
+ Method sanityCheckHeader;
+ try {
+ sanityCheckHeader = LegacyGrid.Header.class
+ .getDeclaredMethod("sanityCheck");
+ sanityCheckHeader.setAccessible(true);
+ Method sanityCheckFooter = LegacyGrid.Footer.class
+ .getDeclaredMethod("sanityCheck");
+ sanityCheckFooter.setAccessible(true);
+ sanityCheckHeader.invoke(getHeader());
+ sanityCheckFooter.invoke(getFooter());
+ } catch (Exception e) {
+ throw e.getCause();
+ }
+ }
+}
diff --git a/compatibility-server/src/test/java/com/vaadin/tests/server/component/grid/MultiSelectionModelTest.java b/compatibility-server/src/test/java/com/vaadin/tests/server/component/grid/MultiSelectionModelTest.java
new file mode 100644
index 0000000000..1199486742
--- /dev/null
+++ b/compatibility-server/src/test/java/com/vaadin/tests/server/component/grid/MultiSelectionModelTest.java
@@ -0,0 +1,171 @@
+/*
+ * Copyright 2000-2016 Vaadin Ltd.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not
+ * use this file except in compliance with the License. You may obtain a copy of
+ * the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+package com.vaadin.tests.server.component.grid;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+
+import com.vaadin.ui.LegacyGrid;
+import org.junit.After;
+import org.junit.Assert;
+import org.junit.Before;
+import org.junit.Test;
+
+import com.vaadin.data.Container;
+import com.vaadin.data.util.IndexedContainer;
+import com.vaadin.event.SelectionEvent;
+import com.vaadin.event.SelectionEvent.SelectionListener;
+import com.vaadin.ui.LegacyGrid.MultiSelectionModel;
+import com.vaadin.ui.LegacyGrid.SelectionMode;
+
+public class MultiSelectionModelTest {
+
+ private Object itemId1Present = "itemId1Present";
+ private Object itemId2Present = "itemId2Present";
+ private Object itemId3Present = "itemId3Present";
+
+ private Object itemIdNotPresent = "itemIdNotPresent";
+ private Container.Indexed dataSource;
+ private MultiSelectionModel model;
+ private LegacyGrid grid;
+
+ private boolean expectingEvent = false;
+ private boolean expectingDeselectEvent;
+ private List<Object> select = new ArrayList<Object>();
+ private List<Object> deselect = new ArrayList<Object>();
+
+ @Before
+ public void setUp() {
+ dataSource = createDataSource();
+ grid = new LegacyGrid(dataSource);
+ grid.setSelectionMode(SelectionMode.MULTI);
+ model = (MultiSelectionModel) grid.getSelectionModel();
+ }
+
+ @After
+ public void tearDown() {
+ Assert.assertFalse("Some expected select event did not happen.",
+ expectingEvent);
+ Assert.assertFalse("Some expected deselect event did not happen.",
+ expectingDeselectEvent);
+ }
+
+ private IndexedContainer createDataSource() {
+ final IndexedContainer container = new IndexedContainer();
+ container.addItem(itemId1Present);
+ container.addItem(itemId2Present);
+ container.addItem(itemId3Present);
+ for (int i = 3; i < 10; i++) {
+ container.addItem(new Object());
+ }
+
+ return container;
+ }
+
+ @Test
+ public void testSelectAndDeselectRow() throws Throwable {
+ try {
+ expectSelectEvent(itemId1Present);
+ model.select(itemId1Present);
+ expectDeselectEvent(itemId1Present);
+ model.deselect(itemId1Present);
+ } catch (Exception e) {
+ throw e.getCause();
+ }
+
+ verifyCurrentSelection();
+ }
+
+ @Test
+ public void testAddSelection() throws Throwable {
+ try {
+ expectSelectEvent(itemId1Present);
+ model.select(itemId1Present);
+ expectSelectEvent(itemId2Present);
+ model.select(itemId2Present);
+ } catch (Exception e) {
+ throw e.getCause();
+ }
+
+ verifyCurrentSelection(itemId1Present, itemId2Present);
+ }
+
+ @Test
+ public void testSettingSelection() throws Throwable {
+ try {
+ expectSelectEvent(itemId2Present, itemId1Present);
+ model.setSelected(Arrays
+ .asList(new Object[] { itemId1Present, itemId2Present }));
+ verifyCurrentSelection(itemId1Present, itemId2Present);
+
+ expectDeselectEvent(itemId1Present);
+ expectSelectEvent(itemId3Present);
+ model.setSelected(Arrays
+ .asList(new Object[] { itemId3Present, itemId2Present }));
+ verifyCurrentSelection(itemId3Present, itemId2Present);
+ } catch (Exception e) {
+ throw e.getCause();
+ }
+ }
+
+ private void expectSelectEvent(Object... selectArray) {
+ select = Arrays.asList(selectArray);
+ addListener();
+ }
+
+ private void expectDeselectEvent(Object... deselectArray) {
+ deselect = Arrays.asList(deselectArray);
+ addListener();
+ }
+
+ private void addListener() {
+ if (expectingEvent) {
+ return;
+ }
+
+ expectingEvent = true;
+ grid.addSelectionListener(new SelectionListener() {
+
+ @Override
+ public void select(SelectionEvent event) {
+ Assert.assertTrue("Selection did not contain expected items",
+ event.getAdded().containsAll(select));
+ Assert.assertTrue("Selection contained unexpected items",
+ select.containsAll(event.getAdded()));
+ select = new ArrayList<Object>();
+
+ Assert.assertTrue("Deselection did not contain expected items",
+ event.getRemoved().containsAll(deselect));
+ Assert.assertTrue("Deselection contained unexpected items",
+ deselect.containsAll(event.getRemoved()));
+ deselect = new ArrayList<Object>();
+
+ grid.removeSelectionListener(this);
+ expectingEvent = false;
+ }
+ });
+ }
+
+ private void verifyCurrentSelection(Object... selection) {
+ final List<Object> selected = Arrays.asList(selection);
+ if (model.getSelectedRows().containsAll(selected)
+ && selected.containsAll(model.getSelectedRows())) {
+ return;
+ }
+ Assert.fail("Not all items were correctly selected");
+ }
+}
diff --git a/compatibility-server/src/test/java/com/vaadin/tests/server/component/grid/SingleSelectionModelTest.java b/compatibility-server/src/test/java/com/vaadin/tests/server/component/grid/SingleSelectionModelTest.java
new file mode 100644
index 0000000000..3183ad9021
--- /dev/null
+++ b/compatibility-server/src/test/java/com/vaadin/tests/server/component/grid/SingleSelectionModelTest.java
@@ -0,0 +1,153 @@
+/*
+ * Copyright 2000-2016 Vaadin Ltd.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not
+ * use this file except in compliance with the License. You may obtain a copy of
+ * the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+package com.vaadin.tests.server.component.grid;
+
+import org.junit.After;
+import org.junit.Assert;
+import org.junit.Before;
+import org.junit.Test;
+
+import com.vaadin.data.Container;
+import com.vaadin.data.util.IndexedContainer;
+import com.vaadin.event.SelectionEvent;
+import com.vaadin.event.SelectionEvent.SelectionListener;
+import com.vaadin.ui.LegacyGrid;
+import com.vaadin.ui.LegacyGrid.SelectionMode;
+import com.vaadin.ui.LegacyGrid.SingleSelectionModel;
+
+public class SingleSelectionModelTest {
+
+ private Object itemId1Present = "itemId1Present";
+ private Object itemId2Present = "itemId2Present";
+
+ private Object itemIdNotPresent = "itemIdNotPresent";
+ private Container.Indexed dataSource;
+ private SingleSelectionModel model;
+ private LegacyGrid grid;
+
+ private boolean expectingEvent = false;
+
+ @Before
+ public void setUp() {
+ dataSource = createDataSource();
+ grid = new LegacyGrid(dataSource);
+ grid.setSelectionMode(SelectionMode.SINGLE);
+ model = (SingleSelectionModel) grid.getSelectionModel();
+ }
+
+ @After
+ public void tearDown() {
+ Assert.assertFalse("Some expected event did not happen.",
+ expectingEvent);
+ }
+
+ private IndexedContainer createDataSource() {
+ final IndexedContainer container = new IndexedContainer();
+ container.addItem(itemId1Present);
+ container.addItem(itemId2Present);
+ for (int i = 2; i < 10; i++) {
+ container.addItem(new Object());
+ }
+
+ return container;
+ }
+
+ @Test
+ public void testSelectAndDeselctRow() throws Throwable {
+ try {
+ expectEvent(itemId1Present, null);
+ model.select(itemId1Present);
+ expectEvent(null, itemId1Present);
+ model.select(null);
+ } catch (Exception e) {
+ throw e.getCause();
+ }
+ }
+
+ @Test
+ public void testSelectAndChangeSelectedRow() throws Throwable {
+ try {
+ expectEvent(itemId1Present, null);
+ model.select(itemId1Present);
+ expectEvent(itemId2Present, itemId1Present);
+ model.select(itemId2Present);
+ } catch (Exception e) {
+ throw e.getCause();
+ }
+ }
+
+ @Test
+ public void testRemovingSelectedRowAndThenDeselecting() throws Throwable {
+ try {
+ expectEvent(itemId2Present, null);
+ model.select(itemId2Present);
+ dataSource.removeItem(itemId2Present);
+ expectEvent(null, itemId2Present);
+ model.select(null);
+ } catch (Exception e) {
+ throw e.getCause();
+ }
+ }
+
+ @Test
+ public void testSelectAndReSelectRow() throws Throwable {
+ try {
+ expectEvent(itemId1Present, null);
+ model.select(itemId1Present);
+ expectEvent(null, null);
+ // This is no-op. Nothing should happen.
+ model.select(itemId1Present);
+ } catch (Exception e) {
+ throw e.getCause();
+ }
+ Assert.assertTrue("Should still wait for event", expectingEvent);
+ expectingEvent = false;
+ }
+
+ @Test(expected = IllegalArgumentException.class)
+ public void testSelectNonExistentRow() {
+ model.select(itemIdNotPresent);
+ }
+
+ private void expectEvent(final Object selected, final Object deselected) {
+ expectingEvent = true;
+ grid.addSelectionListener(new SelectionListener() {
+
+ @Override
+ public void select(SelectionEvent event) {
+ if (selected != null) {
+ Assert.assertTrue("Selection did not contain expected item",
+ event.getAdded().contains(selected));
+ } else {
+ Assert.assertTrue("Unexpected selection",
+ event.getAdded().isEmpty());
+ }
+
+ if (deselected != null) {
+ Assert.assertTrue(
+ "DeSelection did not contain expected item",
+ event.getRemoved().contains(deselected));
+ } else {
+ Assert.assertTrue("Unexpected selection",
+ event.getRemoved().isEmpty());
+ }
+
+ grid.removeSelectionListener(this);
+ expectingEvent = false;
+ }
+ });
+ }
+}
diff --git a/compatibility-server/src/test/java/com/vaadin/tests/server/component/grid/TestGrid.java b/compatibility-server/src/test/java/com/vaadin/tests/server/component/grid/TestGrid.java
new file mode 100644
index 0000000000..9b2dde4d24
--- /dev/null
+++ b/compatibility-server/src/test/java/com/vaadin/tests/server/component/grid/TestGrid.java
@@ -0,0 +1,62 @@
+/*
+ * Copyright 2000-2016 Vaadin Ltd.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not
+ * use this file except in compliance with the License. You may obtain a copy of
+ * the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+package com.vaadin.tests.server.component.grid;
+
+import java.lang.reflect.Field;
+
+import org.easymock.EasyMock;
+
+import com.vaadin.data.util.IndexedContainer;
+import com.vaadin.server.communication.data.RpcDataProviderExtension;
+import com.vaadin.ui.ConnectorTracker;
+import com.vaadin.ui.LegacyGrid;
+import com.vaadin.ui.UI;
+
+/**
+ * A Grid attached to a mock UI with a mock ConnectorTracker.
+ *
+ * @since 7.4
+ * @author Vaadin Ltd
+ */
+public class TestGrid extends LegacyGrid {
+
+ public TestGrid() {
+ super();
+ init();
+ }
+
+ public TestGrid(IndexedContainer c) {
+ super(c);
+ init();
+ }
+
+ public RpcDataProviderExtension getDataProvider() throws Exception {
+ Field dseField = LegacyGrid.class.getDeclaredField("datasourceExtension");
+ dseField.setAccessible(true);
+ return (RpcDataProviderExtension) dseField.get(this);
+ }
+
+ private void init() {
+ UI mockUI = EasyMock.createNiceMock(UI.class);
+ ConnectorTracker mockCT = EasyMock
+ .createNiceMock(ConnectorTracker.class);
+ EasyMock.expect(mockUI.getConnectorTracker()).andReturn(mockCT)
+ .anyTimes();
+ EasyMock.replay(mockUI, mockCT);
+
+ setParent(mockUI);
+ }
+}
diff --git a/compatibility-server/src/test/java/com/vaadin/tests/server/component/grid/declarative/GridColumnDeclarativeTest.java b/compatibility-server/src/test/java/com/vaadin/tests/server/component/grid/declarative/GridColumnDeclarativeTest.java
new file mode 100644
index 0000000000..71d24a2d8e
--- /dev/null
+++ b/compatibility-server/src/test/java/com/vaadin/tests/server/component/grid/declarative/GridColumnDeclarativeTest.java
@@ -0,0 +1,101 @@
+/*
+ * Copyright 2000-2016 Vaadin Ltd.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not
+ * use this file except in compliance with the License. You may obtain a copy of
+ * the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+package com.vaadin.tests.server.component.grid.declarative;
+
+import com.vaadin.ui.LegacyGrid;
+import org.junit.Test;
+
+public class GridColumnDeclarativeTest extends GridDeclarativeTestBase {
+
+ @Test
+ public void testSimpleGridColumns() {
+ String design = "<vaadin-legacy-grid><table>"//
+ + "<colgroup>"
+ + " <col sortable width='100' property-id='Column1'>"
+ + " <col sortable=false max-width='200' expand='2' property-id='Column2'>"
+ + " <col sortable editable=false resizable=false min-width='15' expand='1' property-id='Column3'>"
+ + " <col sortable hidable hiding-toggle-caption='col 4' property-id='Column4'>"
+ + " <col sortable hidden property-id='Column5'>"
+ + "</colgroup>" //
+ + "<thead />" //
+ + "</table></vaadin-legacy-grid>";
+ LegacyGrid grid = new LegacyGrid();
+ grid.addColumn("Column1", String.class).setWidth(100);
+ grid.addColumn("Column2", String.class).setMaximumWidth(200)
+ .setExpandRatio(2).setSortable(false);
+ grid.addColumn("Column3", String.class).setMinimumWidth(15)
+ .setExpandRatio(1).setEditable(false).setResizable(false);
+ grid.addColumn("Column4", String.class).setHidable(true)
+ .setHidingToggleCaption("col 4").setResizable(true);
+ grid.addColumn("Column5", String.class).setHidden(true);
+
+ // Remove the default header
+ grid.removeHeaderRow(grid.getDefaultHeaderRow());
+
+ // Use the read grid component to do another pass on write.
+ testRead(design, grid, true);
+ testWrite(design, grid);
+ }
+
+ @Test
+ public void testReadColumnsWithoutPropertyId() {
+ String design = "<vaadin-legacy-grid><table>"//
+ + "<colgroup>"
+ + " <col sortable=true width='100' property-id='Column1'>"
+ + " <col sortable=true max-width='200' expand='2'>" // property-id="property-1"
+ + " <col sortable=true min-width='15' expand='1' property-id='Column3'>"
+ + " <col sortable=true hidden=true hidable=true hiding-toggle-caption='col 4'>" // property-id="property-3"
+ + "</colgroup>" //
+ + "</table></vaadin-legacy-grid>";
+ LegacyGrid grid = new LegacyGrid();
+ grid.addColumn("Column1", String.class).setWidth(100);
+ grid.addColumn("property-1", String.class).setMaximumWidth(200)
+ .setExpandRatio(2);
+ grid.addColumn("Column3", String.class).setMinimumWidth(15)
+ .setExpandRatio(1);
+ grid.addColumn("property-3", String.class).setHidable(true)
+ .setHidden(true).setHidingToggleCaption("col 4");
+
+ testRead(design, grid);
+ }
+
+ @Test
+ public void testReadEmptyExpand() {
+ String design = "<vaadin-legacy-grid><table>"//
+ + "<colgroup>" + " <col sortable=true expand />"
+ + "</colgroup>" //
+ + "</table></vaadin-legacy-grid>";
+
+ LegacyGrid grid = new LegacyGrid();
+ grid.addColumn("property-0", String.class).setExpandRatio(1);
+
+ testRead(design, grid);
+ }
+
+ @Test
+ public void testReadColumnWithNoAttributes() {
+ String design = "<vaadin-legacy-grid><table>"//
+ + "<colgroup>" //
+ + " <col />" //
+ + "</colgroup>" //
+ + "</table></vaadin-legacy-grid>";
+
+ LegacyGrid grid = new LegacyGrid();
+ grid.addColumn("property-0", String.class);
+
+ testRead(design, grid);
+ }
+}
diff --git a/compatibility-server/src/test/java/com/vaadin/tests/server/component/grid/declarative/GridDeclarativeAttributeTest.java b/compatibility-server/src/test/java/com/vaadin/tests/server/component/grid/declarative/GridDeclarativeAttributeTest.java
new file mode 100644
index 0000000000..8ca99327dc
--- /dev/null
+++ b/compatibility-server/src/test/java/com/vaadin/tests/server/component/grid/declarative/GridDeclarativeAttributeTest.java
@@ -0,0 +1,84 @@
+/*
+ * Copyright 2000-2016 Vaadin Ltd.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not
+ * use this file except in compliance with the License. You may obtain a copy of
+ * the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+package com.vaadin.tests.server.component.grid.declarative;
+
+import static org.junit.Assert.assertSame;
+
+import com.vaadin.ui.LegacyGrid;
+import org.junit.Test;
+
+import com.vaadin.shared.ui.grid.HeightMode;
+import com.vaadin.tests.design.DeclarativeTestBase;
+import com.vaadin.ui.LegacyGrid.MultiSelectionModel;
+import com.vaadin.ui.LegacyGrid.NoSelectionModel;
+import com.vaadin.ui.LegacyGrid.SingleSelectionModel;
+
+/**
+ * Tests declarative support for {@link Grid} properties.
+ *
+ * @since
+ * @author Vaadin Ltd
+ */
+public class GridDeclarativeAttributeTest extends DeclarativeTestBase<LegacyGrid> {
+
+ @Test
+ public void testBasicAttributes() {
+
+ String design = "<vaadin-legacy-grid editable rows=20 frozen-columns=-1 "
+ + "editor-save-caption='Tallenna' editor-cancel-caption='Peruuta' column-reordering-allowed>";
+
+ LegacyGrid grid = new LegacyGrid();
+ grid.setEditorEnabled(true);
+ grid.setHeightMode(HeightMode.ROW);
+ grid.setHeightByRows(20);
+ grid.setFrozenColumnCount(-1);
+ grid.setEditorSaveCaption("Tallenna");
+ grid.setEditorCancelCaption("Peruuta");
+ grid.setColumnReorderingAllowed(true);
+
+ testRead(design, grid);
+ testWrite(design, grid);
+ }
+
+ @Test
+ public void testFrozenColumnsAttributes() {
+ String design = "<vaadin-legacy-grid frozen-columns='2'><table>" //
+ + "<colgroup><col><col><col></colgroup></table></vaadin-legacy-grid>";
+
+ LegacyGrid grid = new LegacyGrid();
+ grid.addColumn("property-0", String.class);
+ grid.addColumn("property-1", String.class);
+ grid.addColumn("property-2", String.class);
+ grid.setFrozenColumnCount(2);
+
+ testRead(design, grid);
+ }
+
+ @Test
+ public void testSelectionMode() {
+ String design = "<vaadin-legacy-grid selection-mode='none'>";
+ assertSame(NoSelectionModel.class,
+ read(design).getSelectionModel().getClass());
+
+ design = "<vaadin-legacy-grid selection-mode='single'>";
+ assertSame(SingleSelectionModel.class,
+ read(design).getSelectionModel().getClass());
+
+ design = "<vaadin-legacy-grid selection-mode='multi'>";
+ assertSame(MultiSelectionModel.class,
+ read(design).getSelectionModel().getClass());
+ }
+}
diff --git a/compatibility-server/src/test/java/com/vaadin/tests/server/component/grid/declarative/GridDeclarativeTestBase.java b/compatibility-server/src/test/java/com/vaadin/tests/server/component/grid/declarative/GridDeclarativeTestBase.java
new file mode 100644
index 0000000000..3e97910be7
--- /dev/null
+++ b/compatibility-server/src/test/java/com/vaadin/tests/server/component/grid/declarative/GridDeclarativeTestBase.java
@@ -0,0 +1,159 @@
+/*
+ * Copyright 2000-2016 Vaadin Ltd.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not
+ * use this file except in compliance with the License. You may obtain a copy of
+ * the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+package com.vaadin.tests.server.component.grid.declarative;
+
+import java.util.List;
+
+import com.vaadin.ui.LegacyGrid;
+import org.junit.Assert;
+
+import com.vaadin.tests.design.DeclarativeTestBase;
+import com.vaadin.ui.LegacyGrid.Column;
+import com.vaadin.ui.LegacyGrid.FooterCell;
+import com.vaadin.ui.LegacyGrid.FooterRow;
+import com.vaadin.ui.LegacyGrid.HeaderCell;
+import com.vaadin.ui.LegacyGrid.HeaderRow;
+
+public class GridDeclarativeTestBase extends DeclarativeTestBase<LegacyGrid> {
+
+ @Override
+ public LegacyGrid testRead(String design, LegacyGrid expected) {
+ return testRead(design, expected, false);
+ }
+
+ public LegacyGrid testRead(String design, LegacyGrid expected, boolean retestWrite) {
+ return testRead(design, expected, retestWrite, false);
+ }
+
+ public LegacyGrid testRead(String design, LegacyGrid expected, boolean retestWrite,
+ boolean writeData) {
+ LegacyGrid actual = super.testRead(design, expected);
+
+ compareGridColumns(expected, actual);
+ compareHeaders(expected, actual);
+ compareFooters(expected, actual);
+
+ if (retestWrite) {
+ testWrite(design, actual, writeData);
+ }
+
+ return actual;
+ }
+
+ private void compareHeaders(LegacyGrid expected, LegacyGrid actual) {
+ Assert.assertEquals("Different header row count",
+ expected.getHeaderRowCount(), actual.getHeaderRowCount());
+ for (int i = 0; i < expected.getHeaderRowCount(); ++i) {
+ HeaderRow expectedRow = expected.getHeaderRow(i);
+ HeaderRow actualRow = actual.getHeaderRow(i);
+
+ if (expectedRow.equals(expected.getDefaultHeaderRow())) {
+ Assert.assertEquals("Different index for default header row",
+ actual.getDefaultHeaderRow(), actualRow);
+ }
+
+ for (Column c : expected.getColumns()) {
+ String baseError = "Difference when comparing cell for "
+ + c.toString() + " on header row " + i + ": ";
+ Object propertyId = c.getPropertyId();
+ HeaderCell expectedCell = expectedRow.getCell(propertyId);
+ HeaderCell actualCell = actualRow.getCell(propertyId);
+
+ switch (expectedCell.getCellType()) {
+ case TEXT:
+ Assert.assertEquals(baseError + "Text content",
+ expectedCell.getText(), actualCell.getText());
+ break;
+ case HTML:
+ Assert.assertEquals(baseError + "HTML content",
+ expectedCell.getHtml(), actualCell.getHtml());
+ break;
+ case WIDGET:
+ assertEquals(baseError + "Component content",
+ expectedCell.getComponent(),
+ actualCell.getComponent());
+ break;
+ }
+ }
+ }
+ }
+
+ private void compareFooters(LegacyGrid expected, LegacyGrid actual) {
+ Assert.assertEquals("Different footer row count",
+ expected.getFooterRowCount(), actual.getFooterRowCount());
+ for (int i = 0; i < expected.getFooterRowCount(); ++i) {
+ FooterRow expectedRow = expected.getFooterRow(i);
+ FooterRow actualRow = actual.getFooterRow(i);
+
+ for (Column c : expected.getColumns()) {
+ String baseError = "Difference when comparing cell for "
+ + c.toString() + " on footer row " + i + ": ";
+ Object propertyId = c.getPropertyId();
+ FooterCell expectedCell = expectedRow.getCell(propertyId);
+ FooterCell actualCell = actualRow.getCell(propertyId);
+
+ switch (expectedCell.getCellType()) {
+ case TEXT:
+ Assert.assertEquals(baseError + "Text content",
+ expectedCell.getText(), actualCell.getText());
+ break;
+ case HTML:
+ Assert.assertEquals(baseError + "HTML content",
+ expectedCell.getHtml(), actualCell.getHtml());
+ break;
+ case WIDGET:
+ assertEquals(baseError + "Component content",
+ expectedCell.getComponent(),
+ actualCell.getComponent());
+ break;
+ }
+ }
+ }
+ }
+
+ private void compareGridColumns(LegacyGrid expected, LegacyGrid actual) {
+ List<Column> columns = expected.getColumns();
+ List<Column> actualColumns = actual.getColumns();
+ Assert.assertEquals("Different amount of columns", columns.size(),
+ actualColumns.size());
+ for (int i = 0; i < columns.size(); ++i) {
+ Column col1 = columns.get(i);
+ Column col2 = actualColumns.get(i);
+ String baseError = "Error when comparing columns for property "
+ + col1.getPropertyId() + ": ";
+ assertEquals(baseError + "Property id", col1.getPropertyId(),
+ col2.getPropertyId());
+ assertEquals(baseError + "Width", col1.getWidth(), col2.getWidth());
+ assertEquals(baseError + "Maximum width", col1.getMaximumWidth(),
+ col2.getMaximumWidth());
+ assertEquals(baseError + "Minimum width", col1.getMinimumWidth(),
+ col2.getMinimumWidth());
+ assertEquals(baseError + "Expand ratio", col1.getExpandRatio(),
+ col2.getExpandRatio());
+ assertEquals(baseError + "Sortable", col1.isSortable(),
+ col2.isSortable());
+ assertEquals(baseError + "Editable", col1.isEditable(),
+ col2.isEditable());
+ assertEquals(baseError + "Hidable", col1.isHidable(),
+ col2.isHidable());
+ assertEquals(baseError + "Hidden", col1.isHidden(),
+ col2.isHidden());
+ assertEquals(baseError + "HidingToggleCaption",
+ col1.getHidingToggleCaption(),
+ col2.getHidingToggleCaption());
+ }
+ }
+}
diff --git a/compatibility-server/src/test/java/com/vaadin/tests/server/component/grid/declarative/GridHeaderFooterDeclarativeTest.java b/compatibility-server/src/test/java/com/vaadin/tests/server/component/grid/declarative/GridHeaderFooterDeclarativeTest.java
new file mode 100644
index 0000000000..1207063c16
--- /dev/null
+++ b/compatibility-server/src/test/java/com/vaadin/tests/server/component/grid/declarative/GridHeaderFooterDeclarativeTest.java
@@ -0,0 +1,356 @@
+/*
+ * Copyright 2000-2016 Vaadin Ltd.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not
+ * use this file except in compliance with the License. You may obtain a copy of
+ * the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+package com.vaadin.tests.server.component.grid.declarative;
+
+import com.vaadin.ui.LegacyGrid;
+import org.jsoup.nodes.Element;
+import org.jsoup.parser.Tag;
+import org.junit.Assert;
+import org.junit.Test;
+
+import com.vaadin.shared.ui.label.ContentMode;
+import com.vaadin.ui.LegacyGrid.Column;
+import com.vaadin.ui.LegacyGrid.FooterRow;
+import com.vaadin.ui.LegacyGrid.HeaderRow;
+import com.vaadin.ui.Label;
+import com.vaadin.ui.declarative.DesignContext;
+
+public class GridHeaderFooterDeclarativeTest extends GridDeclarativeTestBase {
+
+ @Test
+ public void testSingleDefaultHeader() {
+ //@formatter:off
+ String design = "<vaadin-legacy-grid><table>"
+ + "<colgroup>"
+ + " <col sortable property-id='Column1'>"
+ + " <col sortable property-id='Column2'>"
+ + " <col sortable property-id='Column3'>"
+ + "</colgroup>"
+ + "<thead>"
+ + " <tr default><th plain-text>Column1<th plain-text>Column2<th plain-text>Column3</tr>"
+ + "</thead>"
+ + "</table></vaadin-legacy-grid>";
+ //@formatter:on
+ LegacyGrid grid = new LegacyGrid();
+ grid.addColumn("Column1", String.class);
+ grid.addColumn("Column2", String.class);
+ grid.addColumn("Column3", String.class);
+
+ testWrite(design, grid);
+ testRead(design, grid, true);
+ }
+
+ @Test
+ public void testSingleDefaultHTMLHeader() {
+ //@formatter:off
+ String design = "<vaadin-legacy-grid><table>"
+ + "<colgroup>"
+ + " <col sortable property-id='Column1'>"
+ + " <col sortable property-id='Column2'>"
+ + " <col sortable property-id='Column3'>" + "</colgroup>"
+ + "<thead>"
+ + " <tr default><th>Column1<th>Column2<th>Column3</tr>"
+ + "</thead>"
+ + "</table></vaadin-legacy-grid>";
+ //@formatter:on
+ LegacyGrid grid = new LegacyGrid();
+ grid.addColumn("Column1", String.class);
+ grid.addColumn("Column2", String.class);
+ grid.addColumn("Column3", String.class);
+
+ HeaderRow row = grid.getDefaultHeaderRow();
+ for (Column c : grid.getColumns()) {
+ row.getCell(c.getPropertyId()).setHtml(c.getHeaderCaption());
+ }
+
+ testWrite(design, grid);
+ testRead(design, grid, true);
+ }
+
+ @Test
+ public void testNoHeaderRows() {
+ //@formatter:off
+ String design = "<vaadin-legacy-grid><table>"
+ + "<colgroup>"
+ + " <col sortable property-id='Column1'>"
+ + "</colgroup>"
+ + "<thead />"
+ + "</table></vaadin-legacy-grid>";
+ //@formatter:on
+ LegacyGrid grid = new LegacyGrid();
+ grid.addColumn("Column1", String.class);
+ grid.removeHeaderRow(grid.getDefaultHeaderRow());
+
+ testWrite(design, grid);
+ testRead(design, grid, true);
+ }
+
+ @Test
+ public void testMultipleHeadersWithColSpans() {
+ //@formatter:off
+ String design = "<vaadin-legacy-grid><table>"
+ + "<colgroup>"
+ + " <col sortable property-id='Column1'>"
+ + " <col sortable property-id='Column2'>"
+ + " <col sortable property-id='Column3'>"
+ + "</colgroup>"
+ + "<thead>"
+ + " <tr><th colspan=3>Baz</tr>"
+ + " <tr default><th>Column1<th>Column2<th>Column3</tr>"
+ + " <tr><th>Foo<th colspan=2>Bar</tr>"
+ + "</thead>"
+ + "</table></vaadin-legacy-grid>";
+ //@formatter:on
+ LegacyGrid grid = new LegacyGrid();
+ grid.addColumn("Column1", String.class);
+ grid.addColumn("Column2", String.class);
+ grid.addColumn("Column3", String.class);
+
+ HeaderRow row = grid.getDefaultHeaderRow();
+ for (Column c : grid.getColumns()) {
+ row.getCell(c.getPropertyId()).setHtml(c.getHeaderCaption());
+ }
+
+ grid.prependHeaderRow().join("Column1", "Column2", "Column3")
+ .setHtml("Baz");
+ row = grid.appendHeaderRow();
+ row.getCell("Column1").setHtml("Foo");
+ row.join("Column2", "Column3").setHtml("Bar");
+
+ testWrite(design, grid);
+ testRead(design, grid, true);
+ }
+
+ @Test
+ public void testSingleDefaultFooter() {
+ //@formatter:off
+ String design = "<vaadin-legacy-grid><table>"
+ + "<colgroup>"
+ + " <col sortable property-id='Column1'>"
+ + " <col sortable property-id='Column2'>"
+ + " <col sortable property-id='Column3'>"
+ + "</colgroup>"
+ + "<thead />" // No headers read or written
+ + "<tfoot>"
+ + " <tr><td plain-text>Column1<td plain-text>Column2<td plain-text>Column3</tr>"
+ + "</tfoot>"
+ + "</table></vaadin-legacy-grid>";
+ //@formatter:on
+ LegacyGrid grid = new LegacyGrid();
+ grid.addColumn("Column1", String.class);
+ grid.addColumn("Column2", String.class);
+ grid.addColumn("Column3", String.class);
+
+ FooterRow row = grid.appendFooterRow();
+ for (Column c : grid.getColumns()) {
+ row.getCell(c.getPropertyId()).setText(c.getHeaderCaption());
+ }
+
+ grid.removeHeaderRow(grid.getDefaultHeaderRow());
+
+ testWrite(design, grid);
+ testRead(design, grid, true);
+ }
+
+ @Test
+ public void testSingleDefaultHTMLFooter() {
+ //@formatter:off
+ String design = "<vaadin-legacy-grid><table>"
+ + "<colgroup>"
+ + " <col sortable property-id='Column1'>"
+ + " <col sortable property-id='Column2'>"
+ + " <col sortable property-id='Column3'>" + "</colgroup>"
+ + "<thead />" // No headers read or written
+ + "<tfoot>"
+ + " <tr><td>Column1<td>Column2<td>Column3</tr>"
+ + "</tfoot>"
+ + "</table></vaadin-legacy-grid>";
+ //@formatter:on
+
+ LegacyGrid grid = new LegacyGrid();
+ grid.addColumn("Column1", String.class);
+ grid.addColumn("Column2", String.class);
+ grid.addColumn("Column3", String.class);
+
+ FooterRow row = grid.appendFooterRow();
+ for (Column c : grid.getColumns()) {
+ row.getCell(c.getPropertyId()).setHtml(c.getHeaderCaption());
+ }
+
+ grid.removeHeaderRow(grid.getDefaultHeaderRow());
+
+ testWrite(design, grid);
+ testRead(design, grid, true);
+ }
+
+ @Test
+ public void testMultipleFootersWithColSpans() {
+ //@formatter:off
+ String design = "<vaadin-legacy-grid><table>"
+ + "<colgroup>"
+ + " <col sortable property-id='Column1'>"
+ + " <col sortable property-id='Column2'>"
+ + " <col sortable property-id='Column3'>"
+ + "</colgroup>"
+ + "<thead />" // No headers read or written.
+ + "<tfoot>"
+ + " <tr><td colspan=3>Baz</tr>"
+ + " <tr><td>Column1<td>Column2<td>Column3</tr>"
+ + " <tr><td>Foo<td colspan=2>Bar</tr>"
+ + "</tfoot>"
+ + "</table></vaadin-legacy-grid>";
+ //@formatter:on
+
+ LegacyGrid grid = new LegacyGrid();
+ grid.addColumn("Column1", String.class);
+ grid.addColumn("Column2", String.class);
+ grid.addColumn("Column3", String.class);
+
+ FooterRow row = grid.appendFooterRow();
+ for (Column c : grid.getColumns()) {
+ row.getCell(c.getPropertyId()).setHtml(c.getHeaderCaption());
+ }
+
+ grid.prependFooterRow().join("Column1", "Column2", "Column3")
+ .setHtml("Baz");
+ row = grid.appendFooterRow();
+ row.getCell("Column1").setHtml("Foo");
+ row.join("Column2", "Column3").setHtml("Bar");
+
+ grid.removeHeaderRow(grid.getDefaultHeaderRow());
+
+ testWrite(design, grid);
+ testRead(design, grid, true);
+ }
+
+ @Test
+ public void testComponentInGridHeader() {
+ //@formatter:off
+ String design = "<vaadin-legacy-grid><table>"
+ + "<colgroup>"
+ + " <col sortable property-id='Column1'>"
+ + "</colgroup>"
+ + "<thead>"
+ + "<tr default><th><vaadin-label><b>Foo</b></vaadin-label></tr>"
+ + "</thead>"
+ + "</table></vaadin-legacy-grid>";
+ //@formatter:on
+ Label component = new Label("<b>Foo</b>");
+ component.setContentMode(ContentMode.HTML);
+
+ LegacyGrid grid = new LegacyGrid();
+ grid.addColumn("Column1", String.class);
+ grid.getDefaultHeaderRow().getCell("Column1").setComponent(component);
+
+ testRead(design, grid, true);
+ testWrite(design, grid);
+ }
+
+ @Test
+ public void testComponentInGridFooter() {
+ //@formatter:off
+ String design = "<vaadin-legacy-grid><table>"
+ + "<colgroup>"
+ + " <col sortable property-id='Column1'>"
+ + "</colgroup>"
+ + "<thead />" // No headers read or written
+ + "<tfoot>"
+ + "<tr><td><vaadin-label><b>Foo</b></vaadin-label></tr>"
+ + "</tfoot>"
+ + "</table></vaadin-legacy-grid>";
+ //@formatter:on
+
+ Label component = new Label("<b>Foo</b>");
+ component.setContentMode(ContentMode.HTML);
+
+ LegacyGrid grid = new LegacyGrid();
+ grid.addColumn("Column1", String.class);
+ grid.prependFooterRow().getCell("Column1").setComponent(component);
+ grid.removeHeaderRow(grid.getDefaultHeaderRow());
+
+ testRead(design, grid, true);
+ testWrite(design, grid);
+ }
+
+ @Test
+ public void testHtmlEntitiesinGridHeaderFooter() {
+ //@formatter:off
+ String design = "<vaadin-legacy-grid><table>"
+ + "<colgroup>"
+ + " <col sortable=\"true\" property-id=\"> test\">"
+ + "</colgroup>"
+ + "<thead>"
+ + " <tr><th plain-text=\"true\">&gt; Test</th></tr>"
+ + "</thead>"
+ + "<tfoot>"
+ + " <tr><td plain-text=\"true\">&gt; Test</td></tr>"
+ + "</tfoot>"
+ + "<tbody />"
+ + "</table></vaadin-legacy-grid>";
+ //@formatter:on
+
+ LegacyGrid grid = read(design);
+ String actualHeader = grid.getHeaderRow(0).getCell("> test").getText();
+ String actualFooter = grid.getFooterRow(0).getCell("> test").getText();
+ String expected = "> Test";
+
+ Assert.assertEquals(expected, actualHeader);
+ Assert.assertEquals(expected, actualFooter);
+
+ design = design.replace("plain-text=\"true\"", "");
+ grid = read(design);
+ actualHeader = grid.getHeaderRow(0).getCell("> test").getHtml();
+ actualFooter = grid.getFooterRow(0).getCell("> test").getHtml();
+ expected = "&gt; Test";
+
+ Assert.assertEquals(expected, actualHeader);
+ Assert.assertEquals(expected, actualFooter);
+
+ grid = new LegacyGrid();
+ grid.setColumns("test");
+ HeaderRow header = grid.addHeaderRowAt(0);
+ FooterRow footer = grid.addFooterRowAt(0);
+ grid.removeHeaderRow(grid.getDefaultHeaderRow());
+
+ // entities should be encoded when writing back, not interpreted as HTML
+ header.getCell("test").setText("&amp; Test");
+ footer.getCell("test").setText("&amp; Test");
+
+ Element root = new Element(Tag.valueOf("vaadin-legacy-grid"), "");
+ grid.writeDesign(root, new DesignContext());
+
+ Assert.assertEquals("&amp;amp; Test",
+ root.getElementsByTag("th").get(0).html());
+ Assert.assertEquals("&amp;amp; Test",
+ root.getElementsByTag("td").get(0).html());
+
+ header = grid.addHeaderRowAt(0);
+ footer = grid.addFooterRowAt(0);
+
+ // entities should not be encoded, this is already given as HTML
+ header.getCell("test").setHtml("&amp; Test");
+ footer.getCell("test").setHtml("&amp; Test");
+
+ root = new Element(Tag.valueOf("vaadin-legacy-grid"), "");
+ grid.writeDesign(root, new DesignContext());
+
+ Assert.assertEquals("&amp; Test",
+ root.getElementsByTag("th").get(0).html());
+ Assert.assertEquals("&amp; Test",
+ root.getElementsByTag("td").get(0).html());
+
+ }
+}
diff --git a/compatibility-server/src/test/java/com/vaadin/tests/server/component/grid/declarative/GridInlineDataDeclarativeTest.java b/compatibility-server/src/test/java/com/vaadin/tests/server/component/grid/declarative/GridInlineDataDeclarativeTest.java
new file mode 100644
index 0000000000..f5b98824cd
--- /dev/null
+++ b/compatibility-server/src/test/java/com/vaadin/tests/server/component/grid/declarative/GridInlineDataDeclarativeTest.java
@@ -0,0 +1,124 @@
+/*
+ * Copyright 2000-2016 Vaadin Ltd.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not
+ * use this file except in compliance with the License. You may obtain a copy of
+ * the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+package com.vaadin.tests.server.component.grid.declarative;
+
+import com.vaadin.ui.LegacyGrid;
+import org.junit.Assert;
+import org.junit.Test;
+
+import com.vaadin.data.Container;
+
+public class GridInlineDataDeclarativeTest extends GridDeclarativeTestBase {
+
+ @Test
+ public void testSimpleInlineData() {
+ String design = "<vaadin-legacy-grid><table>"//
+ + "<colgroup>" + " <col sortable property-id='Col1' />"
+ + "</colgroup>" //
+ + "<thead />" // No headers read or written
+ + "<tbody>" //
+ + "<tr><td>Foo</tr>" //
+ + "<tr><td>Bar</tr>" //
+ + "<tr><td>Baz</tr>" //
+ + "</tbody>" //
+ + "</table></vaadin-legacy-grid>";
+
+ LegacyGrid grid = new LegacyGrid();
+ grid.addColumn("Col1", String.class);
+ grid.addRow("Foo");
+ grid.addRow("Bar");
+ grid.addRow("Baz");
+
+ // Remove default header
+ grid.removeHeaderRow(grid.getDefaultHeaderRow());
+
+ testWrite(design, grid, true);
+ testRead(design, grid, true, true);
+ }
+
+ @Test
+ public void testMultipleColumnsInlineData() {
+ String design = "<vaadin-legacy-grid><table>"//
+ + "<colgroup>" + " <col sortable property-id='Col1' />"
+ + " <col sortable property-id='Col2' />"
+ + " <col sortable property-id='Col3' />" //
+ + "</colgroup>" //
+ + "<thead />" // No headers read or written
+ + "<tbody>" //
+ + "<tr><td>Foo<td>Bar<td>Baz</tr>" //
+ + "<tr><td>My<td>Summer<td>Car</tr>" //
+ + "</tbody>" //
+ + "</table></vaadin-legacy-grid>";
+
+ LegacyGrid grid = new LegacyGrid();
+ grid.addColumn("Col1", String.class);
+ grid.addColumn("Col2", String.class);
+ grid.addColumn("Col3", String.class);
+ grid.addRow("Foo", "Bar", "Baz");
+ grid.addRow("My", "Summer", "Car");
+
+ // Remove default header
+ grid.removeHeaderRow(grid.getDefaultHeaderRow());
+
+ testWrite(design, grid, true);
+ testRead(design, grid, true, true);
+ }
+
+ @Test
+ public void testMultipleColumnsInlineDataReordered() {
+ String design = "<vaadin-legacy-grid><table>"//
+ + "<colgroup>" + " <col sortable property-id='Col2' />"
+ + " <col sortable property-id='Col3' />"
+ + " <col sortable property-id='Col1' />" //
+ + "</colgroup>" //
+ + "<thead />" // No headers read or written
+ + "<tbody>" //
+ + "<tr><td>Bar<td>Baz<td>Foo</tr>" //
+ + "<tr><td>Summer<td>Car<td>My</tr>" //
+ + "</tbody>" //
+ + "</table></vaadin-legacy-grid>";
+
+ LegacyGrid grid = new LegacyGrid();
+ grid.addColumn("Col1", String.class);
+ grid.addColumn("Col2", String.class);
+ grid.addColumn("Col3", String.class);
+ grid.addRow("Foo", "Bar", "Baz");
+ grid.addRow("My", "Summer", "Car");
+ grid.setColumnOrder("Col2", "Col3", "Col1");
+
+ // Remove default header
+ grid.removeHeaderRow(grid.getDefaultHeaderRow());
+
+ testWrite(design, grid, true);
+ testRead(design, grid, true, true);
+ }
+
+ @Test
+ public void testHtmlEntities() {
+ String design = "<vaadin-legacy-grid><table>"//
+ + "<colgroup>" + " <col property-id='test' />" + "</colgroup>" //
+ + "<thead />" // No headers read or written
+ + "<tbody>" //
+ + " <tr><td>&amp;Test</tr></td>" + "</tbody>"
+ + "</table></vaadin-legacy-grid>";
+
+ LegacyGrid read = read(design);
+ Container cds = read.getContainerDataSource();
+ Assert.assertEquals("&amp;Test",
+ cds.getItem(cds.getItemIds().iterator().next())
+ .getItemProperty("test").getValue());
+ }
+}
diff --git a/compatibility-server/src/test/java/com/vaadin/tests/server/component/grid/declarative/GridStructureDeclarativeTest.java b/compatibility-server/src/test/java/com/vaadin/tests/server/component/grid/declarative/GridStructureDeclarativeTest.java
new file mode 100644
index 0000000000..b55b0815f8
--- /dev/null
+++ b/compatibility-server/src/test/java/com/vaadin/tests/server/component/grid/declarative/GridStructureDeclarativeTest.java
@@ -0,0 +1,50 @@
+/*
+ * Copyright 2000-2016 Vaadin Ltd.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not
+ * use this file except in compliance with the License. You may obtain a copy of
+ * the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+package com.vaadin.tests.server.component.grid.declarative;
+
+import com.vaadin.ui.LegacyGrid;
+import org.junit.Test;
+
+import com.vaadin.ui.declarative.DesignException;
+
+public class GridStructureDeclarativeTest extends GridDeclarativeTestBase {
+
+ @Test
+ public void testReadEmptyGrid() {
+ String design = "<vaadin-legacy-grid />";
+ testRead(design, new LegacyGrid(), false);
+ }
+
+ @Test
+ public void testEmptyGrid() {
+ String design = "<vaadin-legacy-grid></vaadin-legacy-grid>";
+ LegacyGrid expected = new LegacyGrid();
+ testWrite(design, expected);
+ testRead(design, expected, true);
+ }
+
+ @Test(expected = DesignException.class)
+ public void testMalformedGrid() {
+ String design = "<vaadin-legacy-grid><vaadin-label /></vaadin-legacy-grid>";
+ testRead(design, new LegacyGrid());
+ }
+
+ @Test(expected = DesignException.class)
+ public void testGridWithNoColGroup() {
+ String design = "<vaadin-legacy-grid><table><thead><tr><th>Foo</tr></thead></table></vaadin-legacy-grid>";
+ testRead(design, new LegacyGrid());
+ }
+}
diff --git a/compatibility-server/src/test/java/com/vaadin/tests/server/component/grid/sort/SortTest.java b/compatibility-server/src/test/java/com/vaadin/tests/server/component/grid/sort/SortTest.java
new file mode 100644
index 0000000000..beb774528f
--- /dev/null
+++ b/compatibility-server/src/test/java/com/vaadin/tests/server/component/grid/sort/SortTest.java
@@ -0,0 +1,205 @@
+/*
+ * Copyright 2000-2016 Vaadin Ltd.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not
+ * use this file except in compliance with the License. You may obtain a copy of
+ * the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+package com.vaadin.tests.server.component.grid.sort;
+
+import java.util.Arrays;
+import java.util.List;
+
+import org.junit.After;
+import org.junit.Assert;
+import org.junit.Before;
+import org.junit.Test;
+
+import com.vaadin.data.sort.Sort;
+import com.vaadin.data.sort.SortOrder;
+import com.vaadin.data.util.IndexedContainer;
+import com.vaadin.event.SortEvent;
+import com.vaadin.event.SortEvent.SortListener;
+import com.vaadin.shared.data.sort.SortDirection;
+import com.vaadin.ui.LegacyGrid;
+
+public class SortTest {
+
+ class DummySortingIndexedContainer extends IndexedContainer {
+
+ private Object[] expectedProperties;
+ private boolean[] expectedAscending;
+ private boolean sorted = true;
+
+ @Override
+ public void sort(Object[] propertyId, boolean[] ascending) {
+ Assert.assertEquals(
+ "Different amount of expected and actual properties,",
+ expectedProperties.length, propertyId.length);
+ Assert.assertEquals(
+ "Different amount of expected and actual directions",
+ expectedAscending.length, ascending.length);
+ for (int i = 0; i < propertyId.length; ++i) {
+ Assert.assertEquals("Sorting properties differ",
+ expectedProperties[i], propertyId[i]);
+ Assert.assertEquals("Sorting directions differ",
+ expectedAscending[i], ascending[i]);
+ }
+ sorted = true;
+ }
+
+ public void expectedSort(Object[] properties,
+ SortDirection[] directions) {
+ assert directions.length == properties.length : "Array dimensions differ";
+ expectedProperties = properties;
+ expectedAscending = new boolean[directions.length];
+ for (int i = 0; i < directions.length; ++i) {
+ expectedAscending[i] = (directions[i] == SortDirection.ASCENDING);
+ }
+ sorted = false;
+ }
+
+ public boolean isSorted() {
+ return sorted;
+ }
+ }
+
+ class RegisteringSortChangeListener implements SortListener {
+ private List<SortOrder> order;
+
+ @Override
+ public void sort(SortEvent event) {
+ assert order == null : "The same listener was notified multipe times without checking";
+
+ order = event.getSortOrder();
+ }
+
+ public void assertEventFired(SortOrder... expectedOrder) {
+ Assert.assertEquals(Arrays.asList(expectedOrder), order);
+
+ // Reset for nest test
+ order = null;
+ }
+
+ }
+
+ private DummySortingIndexedContainer container;
+ private RegisteringSortChangeListener listener;
+ private LegacyGrid grid;
+
+ @Before
+ public void setUp() {
+ container = createContainer();
+ container.expectedSort(new Object[] {}, new SortDirection[] {});
+
+ listener = new RegisteringSortChangeListener();
+
+ grid = new LegacyGrid(container);
+ grid.addSortListener(listener);
+ }
+
+ @After
+ public void tearDown() {
+ Assert.assertTrue("Container was not sorted after the test.",
+ container.isSorted());
+ }
+
+ @Test(expected = IllegalArgumentException.class)
+ public void testInvalidSortDirection() {
+ Sort.by("foo", null);
+ }
+
+ @Test(expected = IllegalStateException.class)
+ public void testSortOneColumnMultipleTimes() {
+ Sort.by("foo").then("bar").then("foo");
+ }
+
+ @Test(expected = IllegalArgumentException.class)
+ public void testSortingByUnexistingProperty() {
+ grid.sort("foobar");
+ }
+
+ @Test(expected = IllegalArgumentException.class)
+ public void testSortingByUnsortableProperty() {
+ container.addContainerProperty("foobar", Object.class, null);
+ grid.sort("foobar");
+ }
+
+ @Test
+ public void testGridDirectSortAscending() {
+ container.expectedSort(new Object[] { "foo" },
+ new SortDirection[] { SortDirection.ASCENDING });
+ grid.sort("foo");
+
+ listener.assertEventFired(
+ new SortOrder("foo", SortDirection.ASCENDING));
+ }
+
+ @Test
+ public void testGridDirectSortDescending() {
+ container.expectedSort(new Object[] { "foo" },
+ new SortDirection[] { SortDirection.DESCENDING });
+ grid.sort("foo", SortDirection.DESCENDING);
+
+ listener.assertEventFired(
+ new SortOrder("foo", SortDirection.DESCENDING));
+ }
+
+ @Test
+ public void testGridSortBy() {
+ container.expectedSort(new Object[] { "foo", "bar", "baz" },
+ new SortDirection[] { SortDirection.ASCENDING,
+ SortDirection.ASCENDING, SortDirection.DESCENDING });
+ grid.sort(Sort.by("foo").then("bar").then("baz",
+ SortDirection.DESCENDING));
+
+ listener.assertEventFired(new SortOrder("foo", SortDirection.ASCENDING),
+ new SortOrder("bar", SortDirection.ASCENDING),
+ new SortOrder("baz", SortDirection.DESCENDING));
+
+ }
+
+ @Test
+ public void testChangeContainerAfterSorting() {
+ class Person {
+ }
+
+ container.expectedSort(new Object[] { "foo", "bar", "baz" },
+ new SortDirection[] { SortDirection.ASCENDING,
+ SortDirection.ASCENDING, SortDirection.DESCENDING });
+ grid.sort(Sort.by("foo").then("bar").then("baz",
+ SortDirection.DESCENDING));
+
+ listener.assertEventFired(new SortOrder("foo", SortDirection.ASCENDING),
+ new SortOrder("bar", SortDirection.ASCENDING),
+ new SortOrder("baz", SortDirection.DESCENDING));
+
+ container = new DummySortingIndexedContainer();
+ container.addContainerProperty("foo", Person.class, null);
+ container.addContainerProperty("baz", String.class, "");
+ container.addContainerProperty("bar", Person.class, null);
+ container.expectedSort(new Object[] { "baz" },
+ new SortDirection[] { SortDirection.DESCENDING });
+ grid.setContainerDataSource(container);
+
+ listener.assertEventFired(
+ new SortOrder("baz", SortDirection.DESCENDING));
+
+ }
+
+ private DummySortingIndexedContainer createContainer() {
+ DummySortingIndexedContainer container = new DummySortingIndexedContainer();
+ container.addContainerProperty("foo", Integer.class, 0);
+ container.addContainerProperty("bar", Integer.class, 0);
+ container.addContainerProperty("baz", Integer.class, 0);
+ return container;
+ }
+}
diff --git a/compatibility-server/src/test/java/com/vaadin/tests/server/renderer/ImageRendererTest.java b/compatibility-server/src/test/java/com/vaadin/tests/server/renderer/ImageRendererTest.java
new file mode 100644
index 0000000000..822a2353ac
--- /dev/null
+++ b/compatibility-server/src/test/java/com/vaadin/tests/server/renderer/ImageRendererTest.java
@@ -0,0 +1,85 @@
+/*
+ * Copyright 2000-2016 Vaadin Ltd.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not
+ * use this file except in compliance with the License. You may obtain a copy of
+ * the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+package com.vaadin.tests.server.renderer;
+
+import static org.junit.Assert.assertEquals;
+
+import java.io.File;
+
+import org.easymock.EasyMock;
+import org.junit.Before;
+import org.junit.Test;
+
+import com.vaadin.server.ClassResource;
+import com.vaadin.server.ExternalResource;
+import com.vaadin.server.FileResource;
+import com.vaadin.server.FontAwesome;
+import com.vaadin.server.ThemeResource;
+import com.vaadin.ui.LegacyGrid;
+import com.vaadin.ui.UI;
+import com.vaadin.ui.renderers.ImageRenderer;
+
+import elemental.json.JsonObject;
+import elemental.json.JsonValue;
+
+public class ImageRendererTest {
+
+ private ImageRenderer renderer;
+
+ @Before
+ public void setUp() {
+ UI mockUI = EasyMock.createNiceMock(UI.class);
+ EasyMock.replay(mockUI);
+
+ LegacyGrid grid = new LegacyGrid();
+ grid.setParent(mockUI);
+
+ renderer = new ImageRenderer();
+ renderer.setParent(grid);
+ }
+
+ @Test
+ public void testThemeResource() {
+ JsonValue v = renderer.encode(new ThemeResource("foo.png"));
+ assertEquals("theme://foo.png", getUrl(v));
+ }
+
+ @Test
+ public void testExternalResource() {
+ JsonValue v = renderer
+ .encode(new ExternalResource("http://example.com/foo.png"));
+ assertEquals("http://example.com/foo.png", getUrl(v));
+ }
+
+ @Test(expected = IllegalArgumentException.class)
+ public void testFileResource() {
+ renderer.encode(new FileResource(new File("/tmp/foo.png")));
+ }
+
+ @Test(expected = IllegalArgumentException.class)
+ public void testClassResource() {
+ renderer.encode(new ClassResource("img/foo.png"));
+ }
+
+ @Test(expected = IllegalArgumentException.class)
+ public void testFontIcon() {
+ renderer.encode(FontAwesome.AMBULANCE);
+ }
+
+ private String getUrl(JsonValue v) {
+ return ((JsonObject) v).get("uRL").asString();
+ }
+}
diff --git a/compatibility-server/src/test/java/com/vaadin/tests/server/renderer/RendererTest.java b/compatibility-server/src/test/java/com/vaadin/tests/server/renderer/RendererTest.java
new file mode 100644
index 0000000000..5eec5b6ea8
--- /dev/null
+++ b/compatibility-server/src/test/java/com/vaadin/tests/server/renderer/RendererTest.java
@@ -0,0 +1,268 @@
+/*
+ * Copyright 2000-2016 Vaadin Ltd.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not
+ * use this file except in compliance with the License. You may obtain a copy of
+ * the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+package com.vaadin.tests.server.renderer;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertSame;
+
+import java.util.Date;
+import java.util.Locale;
+
+import org.junit.Before;
+import org.junit.Test;
+
+import com.vaadin.data.Item;
+import com.vaadin.data.util.IndexedContainer;
+import com.vaadin.server.VaadinSession;
+import com.vaadin.tests.server.component.grid.TestGrid;
+import com.vaadin.tests.util.AlwaysLockedVaadinSession;
+import com.vaadin.ui.LegacyGrid;
+import com.vaadin.ui.LegacyGrid.AbstractRenderer;
+import com.vaadin.ui.LegacyGrid.Column;
+import com.vaadin.ui.renderers.ButtonRenderer;
+import com.vaadin.ui.renderers.DateRenderer;
+import com.vaadin.ui.renderers.HtmlRenderer;
+import com.vaadin.ui.renderers.NumberRenderer;
+import com.vaadin.ui.renderers.TextRenderer;
+import com.vaadin.v7.data.util.converter.LegacyConverter;
+import com.vaadin.v7.data.util.converter.LegacyStringToIntegerConverter;
+
+import elemental.json.JsonValue;
+
+public class RendererTest {
+
+ private static class TestBean {
+ int i = 42;
+
+ @Override
+ public String toString() {
+ return "TestBean [" + i + "]";
+ }
+ }
+
+ private static class ExtendedBean extends TestBean {
+ float f = 3.14f;
+ }
+
+ private static class TestRenderer extends TextRenderer {
+ @Override
+ public JsonValue encode(String value) {
+ return super.encode("renderer(" + value + ")");
+ }
+ }
+
+ private static class TestConverter
+ implements LegacyConverter<String, TestBean> {
+
+ @Override
+ public TestBean convertToModel(String value,
+ Class<? extends TestBean> targetType, Locale locale)
+ throws ConversionException {
+ return null;
+ }
+
+ @Override
+ public String convertToPresentation(TestBean value,
+ Class<? extends String> targetType, Locale locale)
+ throws ConversionException {
+ if (value instanceof ExtendedBean) {
+ return "ExtendedBean(" + value.i + ", "
+ + ((ExtendedBean) value).f + ")";
+ } else {
+ return "TestBean(" + value.i + ")";
+ }
+ }
+
+ @Override
+ public Class<TestBean> getModelType() {
+ return TestBean.class;
+ }
+
+ @Override
+ public Class<String> getPresentationType() {
+ return String.class;
+ }
+ }
+
+ private LegacyGrid grid;
+
+ private Column intColumn;
+ private Column textColumn;
+ private Column beanColumn;
+ private Column htmlColumn;
+ private Column numberColumn;
+ private Column dateColumn;
+ private Column extendedBeanColumn;
+ private Column buttonColumn;
+
+ @Before
+ @SuppressWarnings("unchecked")
+ public void setUp() {
+ VaadinSession.setCurrent(new AlwaysLockedVaadinSession(null));
+
+ IndexedContainer c = new IndexedContainer();
+
+ c.addContainerProperty("int", Integer.class, 0);
+ c.addContainerProperty("text", String.class, "");
+ c.addContainerProperty("html", String.class, "");
+ c.addContainerProperty("number", Number.class, null);
+ c.addContainerProperty("date", Date.class, null);
+ c.addContainerProperty("bean", TestBean.class, null);
+ c.addContainerProperty("button", String.class, null);
+ c.addContainerProperty("extendedBean", ExtendedBean.class, null);
+
+ Object id = c.addItem();
+ Item item = c.getItem(id);
+ item.getItemProperty("int").setValue(123);
+ item.getItemProperty("text").setValue("321");
+ item.getItemProperty("html").setValue("<b>html</b>");
+ item.getItemProperty("number").setValue(3.14);
+ item.getItemProperty("date").setValue(new Date(123456789));
+ item.getItemProperty("bean").setValue(new TestBean());
+ item.getItemProperty("extendedBean").setValue(new ExtendedBean());
+
+ grid = new TestGrid(c);
+
+ intColumn = grid.getColumn("int");
+ textColumn = grid.getColumn("text");
+ htmlColumn = grid.getColumn("html");
+ numberColumn = grid.getColumn("number");
+ dateColumn = grid.getColumn("date");
+ beanColumn = grid.getColumn("bean");
+ extendedBeanColumn = grid.getColumn("extendedBean");
+ buttonColumn = grid.getColumn("button");
+
+ }
+
+ @Test
+ public void testDefaultRendererAndConverter() throws Exception {
+ assertSame(TextRenderer.class, intColumn.getRenderer().getClass());
+ assertSame(LegacyStringToIntegerConverter.class,
+ intColumn.getConverter().getClass());
+
+ assertSame(TextRenderer.class, textColumn.getRenderer().getClass());
+ // String->String; converter not needed
+ assertNull(textColumn.getConverter());
+
+ assertSame(TextRenderer.class, beanColumn.getRenderer().getClass());
+ // MyBean->String; converter not found
+ assertNull(beanColumn.getConverter());
+ }
+
+ @Test
+ public void testFindCompatibleConverter() throws Exception {
+ intColumn.setRenderer(renderer());
+ assertSame(LegacyStringToIntegerConverter.class,
+ intColumn.getConverter().getClass());
+
+ textColumn.setRenderer(renderer());
+ assertNull(textColumn.getConverter());
+ }
+
+ @Test(expected = IllegalArgumentException.class)
+ public void testCannotFindConverter() {
+ beanColumn.setRenderer(renderer());
+ }
+
+ @Test
+ public void testExplicitConverter() throws Exception {
+ beanColumn.setRenderer(renderer(), converter());
+ extendedBeanColumn.setRenderer(renderer(), converter());
+ }
+
+ @Test
+ public void testEncoding() throws Exception {
+ assertEquals("42", render(intColumn, 42).asString());
+ intColumn.setRenderer(renderer());
+ assertEquals("renderer(42)", render(intColumn, 42).asString());
+
+ assertEquals("2.72", render(textColumn, "2.72").asString());
+ textColumn.setRenderer(new TestRenderer());
+ assertEquals("renderer(2.72)", render(textColumn, "2.72").asString());
+ }
+
+ @Test
+ public void testEncodingWithoutConverter() throws Exception {
+ assertEquals("TestBean [42]",
+ render(beanColumn, new TestBean()).asString());
+ }
+
+ @Test
+ public void testBeanEncoding() throws Exception {
+ beanColumn.setRenderer(renderer(), converter());
+ extendedBeanColumn.setRenderer(renderer(), converter());
+
+ assertEquals("renderer(TestBean(42))",
+ render(beanColumn, new TestBean()).asString());
+ assertEquals("renderer(ExtendedBean(42, 3.14))",
+ render(beanColumn, new ExtendedBean()).asString());
+
+ assertEquals("renderer(ExtendedBean(42, 3.14))",
+ render(extendedBeanColumn, new ExtendedBean()).asString());
+ }
+
+ @Test
+ public void testNullEncoding() {
+
+ textColumn.setRenderer(new TextRenderer());
+ htmlColumn.setRenderer(new HtmlRenderer());
+ numberColumn.setRenderer(new NumberRenderer());
+ dateColumn.setRenderer(new DateRenderer());
+ buttonColumn.setRenderer(new ButtonRenderer());
+
+ assertEquals("", textColumn.getRenderer().encode(null).asString());
+ assertEquals("", htmlColumn.getRenderer().encode(null).asString());
+ assertEquals("", numberColumn.getRenderer().encode(null).asString());
+ assertEquals("", dateColumn.getRenderer().encode(null).asString());
+ assertEquals("", buttonColumn.getRenderer().encode(null).asString());
+ }
+
+ @Test
+ public void testNullEncodingWithDefault() {
+
+ textColumn.setRenderer(new TextRenderer("default value"));
+ htmlColumn.setRenderer(new HtmlRenderer("default value"));
+ numberColumn.setRenderer(
+ new NumberRenderer("%s", Locale.getDefault(), "default value"));
+ dateColumn.setRenderer(new DateRenderer("%s", "default value"));
+ buttonColumn.setRenderer(new ButtonRenderer("default value"));
+
+ assertEquals("default value",
+ textColumn.getRenderer().encode(null).asString());
+ assertEquals("default value",
+ htmlColumn.getRenderer().encode(null).asString());
+ assertEquals("default value",
+ numberColumn.getRenderer().encode(null).asString());
+ assertEquals("default value",
+ dateColumn.getRenderer().encode(null).asString());
+ assertEquals("default value",
+ buttonColumn.getRenderer().encode(null).asString());
+ }
+
+ private TestConverter converter() {
+ return new TestConverter();
+ }
+
+ private TestRenderer renderer() {
+ return new TestRenderer();
+ }
+
+ private JsonValue render(Column column, Object value) {
+ return AbstractRenderer.encodeValue(value, column.getRenderer(),
+ column.getConverter(), grid.getLocale());
+ }
+}
diff --git a/compatibility-server/src/test/java/com/vaadin/tests/server/validation/BeanValidationTest.java b/compatibility-server/src/test/java/com/vaadin/tests/server/validation/BeanValidationTest.java
new file mode 100644
index 0000000000..a8e6238d4b
--- /dev/null
+++ b/compatibility-server/src/test/java/com/vaadin/tests/server/validation/BeanValidationTest.java
@@ -0,0 +1,129 @@
+package com.vaadin.tests.server.validation;
+
+import org.junit.Assert;
+import org.junit.Test;
+
+import com.vaadin.data.fieldgroup.BeanFieldGroup;
+import com.vaadin.tests.data.bean.BeanToValidate;
+import com.vaadin.v7.data.Validator.InvalidValueException;
+import com.vaadin.v7.data.validator.LegacyBeanValidator;
+import com.vaadin.v7.ui.LegacyField;
+
+public class BeanValidationTest {
+ @Test(expected = InvalidValueException.class)
+ public void testBeanValidationNull() {
+ LegacyBeanValidator validator = new LegacyBeanValidator(
+ BeanToValidate.class, "firstname");
+ validator.validate(null);
+ }
+
+ @Test(expected = InvalidValueException.class)
+ public void testBeanValidationStringTooShort() {
+ LegacyBeanValidator validator = new LegacyBeanValidator(
+ BeanToValidate.class, "firstname");
+ validator.validate("aa");
+ }
+
+ @Test
+ public void testBeanValidationStringOk() {
+ LegacyBeanValidator validator = new LegacyBeanValidator(
+ BeanToValidate.class, "firstname");
+ validator.validate("aaa");
+ }
+
+ @Test(expected = InvalidValueException.class)
+ public void testBeanValidationIntegerTooSmall() {
+ LegacyBeanValidator validator = new LegacyBeanValidator(
+ BeanToValidate.class, "age");
+ validator.validate(17);
+ }
+
+ @Test
+ public void testBeanValidationIntegerOk() {
+ LegacyBeanValidator validator = new LegacyBeanValidator(
+ BeanToValidate.class, "age");
+ validator.validate(18);
+ }
+
+ @Test(expected = InvalidValueException.class)
+ public void testBeanValidationTooManyDigits() {
+ LegacyBeanValidator validator = new LegacyBeanValidator(
+ BeanToValidate.class, "decimals");
+ validator.validate("1234.567");
+ }
+
+ @Test
+ public void testBeanValidationDigitsOk() {
+ LegacyBeanValidator validator = new LegacyBeanValidator(
+ BeanToValidate.class, "decimals");
+ validator.validate("123.45");
+ }
+
+ @Test
+ public void testBeanValidationException_OneValidationError() {
+ InvalidValueException[] causes = null;
+ LegacyBeanValidator validator = new LegacyBeanValidator(
+ BeanToValidate.class, "lastname");
+ try {
+ validator.validate(null);
+ } catch (InvalidValueException e) {
+ causes = e.getCauses();
+ }
+
+ Assert.assertEquals(1, causes.length);
+ }
+
+ @Test
+ public void testBeanValidationsException_TwoValidationErrors() {
+ InvalidValueException[] causes = null;
+ LegacyBeanValidator validator = new LegacyBeanValidator(
+ BeanToValidate.class, "nickname");
+ try {
+ validator.validate("A");
+ } catch (InvalidValueException e) {
+ causes = e.getCauses();
+ }
+
+ Assert.assertEquals(2, causes.length);
+ }
+
+ @Test
+ public void testBeanValidationNotAddedTwice() {
+ // See ticket #11045
+ BeanFieldGroup<BeanToValidate> fieldGroup = new BeanFieldGroup<BeanToValidate>(
+ BeanToValidate.class);
+
+ BeanToValidate beanToValidate = new BeanToValidate();
+ beanToValidate.setFirstname("a");
+ fieldGroup.setItemDataSource(beanToValidate);
+
+ LegacyField<?> nameField = fieldGroup.buildAndBind("firstname");
+ Assert.assertEquals(1, nameField.getValidators().size());
+
+ try {
+ nameField.validate();
+ } catch (InvalidValueException e) {
+ // The 1 cause is from BeanValidator, where it tells what failed
+ // 1 validation exception never gets wrapped.
+ Assert.assertEquals(1, e.getCauses().length);
+ }
+
+ // Create new, identical bean to cause duplicate validator unless #11045
+ // is fixed
+ beanToValidate = new BeanToValidate();
+ beanToValidate.setFirstname("a");
+ fieldGroup.setItemDataSource(beanToValidate);
+
+ Assert.assertEquals(1, nameField.getValidators().size());
+
+ try {
+ nameField.validate();
+ } catch (InvalidValueException e) {
+ // The 1 cause is from BeanValidator, where it tells what failed
+ // 1 validation exception never gets wrapped.
+ Assert.assertEquals(1, e.getCauses().length);
+ }
+
+ }
+
+}
diff --git a/compatibility-server/src/test/java/com/vaadin/ui/TableTest.java b/compatibility-server/src/test/java/com/vaadin/ui/TableTest.java
new file mode 100644
index 0000000000..f09313f685
--- /dev/null
+++ b/compatibility-server/src/test/java/com/vaadin/ui/TableTest.java
@@ -0,0 +1,78 @@
+/*
+ * Copyright 2000-2016 Vaadin Ltd.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not
+ * use this file except in compliance with the License. You may obtain a copy of
+ * the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+package com.vaadin.ui;
+
+import java.util.Collection;
+
+import org.junit.Assert;
+import org.junit.Before;
+import org.junit.Test;
+
+import com.vaadin.data.util.BeanItemContainerGenerator;
+
+public class TableTest {
+
+ Table table;
+
+ @Before
+ public void init() {
+ table = new Table();
+ }
+
+ @Test
+ public void initiallyEmpty() {
+ Assert.assertTrue(table.isEmpty());
+ }
+
+ @Test
+ public void emptyAfterClearSingleSelect() {
+ table.setContainerDataSource(
+ BeanItemContainerGenerator.createContainer(100));
+ Assert.assertTrue(table.isEmpty());
+ Object first = table.getContainerDataSource().getItemIds().iterator()
+ .next();
+ table.setValue(first);
+ Assert.assertEquals(first, table.getValue());
+ Assert.assertFalse(table.isEmpty());
+ table.clear();
+ Assert.assertEquals(null, table.getValue());
+ Assert.assertTrue(table.isEmpty());
+ }
+
+ @Test
+ public void emptyAfterClearMultiSelect() {
+ table.setMultiSelect(true);
+ table.setContainerDataSource(
+ BeanItemContainerGenerator.createContainer(100));
+
+ Assert.assertTrue(table.isEmpty());
+ Assert.assertArrayEquals(new Object[] {},
+ ((Collection) table.getValue()).toArray());
+
+ Object first = table.getContainerDataSource().getItemIds().iterator()
+ .next();
+ table.select(first);
+ Assert.assertArrayEquals(new Object[] { first },
+ ((Collection) table.getValue()).toArray());
+ Assert.assertFalse(table.isEmpty());
+
+ table.clear();
+ Assert.assertArrayEquals(new Object[] {},
+ ((Collection) table.getValue()).toArray());
+ Assert.assertTrue(table.isEmpty());
+ }
+
+}