From 5ba20eaf2582655d2609740236672dd2cbc51ddc Mon Sep 17 00:00:00 2001 From: Henrik Paul Date: Mon, 18 Aug 2014 12:43:03 +0300 Subject: [PATCH] Server-side editor row (#13334) Change-Id: Ia84c8f0a00549318e35e2c844b6ec6c419cfa4f3 --- .../vaadin/ui/components/grid/EditorRow.java | 373 ++++++++++++++++++ .../com/vaadin/ui/components/grid/Grid.java | 21 + .../server/component/grid/EditorRowTests.java | 234 +++++++++++ 3 files changed, 628 insertions(+) create mode 100644 server/src/com/vaadin/ui/components/grid/EditorRow.java create mode 100644 server/tests/src/com/vaadin/tests/server/component/grid/EditorRowTests.java diff --git a/server/src/com/vaadin/ui/components/grid/EditorRow.java b/server/src/com/vaadin/ui/components/grid/EditorRow.java new file mode 100644 index 0000000000..5eb38156e2 --- /dev/null +++ b/server/src/com/vaadin/ui/components/grid/EditorRow.java @@ -0,0 +1,373 @@ +/* + * Copyright 2000-2014 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.components.grid; + +import java.io.Serializable; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.HashSet; + +import com.vaadin.data.Container; +import com.vaadin.data.Item; +import com.vaadin.data.fieldgroup.FieldGroup; +import com.vaadin.data.fieldgroup.FieldGroup.BindException; +import com.vaadin.data.fieldgroup.FieldGroup.CommitException; +import com.vaadin.data.fieldgroup.FieldGroupFieldFactory; +import com.vaadin.ui.Field; + +/** + * A class for configuring the editor row in a grid. + * + * @since + * @author Vaadin Ltd + * @see Grid + */ +public class EditorRow implements Serializable { + private final Container container; + + private boolean isEnabled; + private FieldGroup fieldGroup = new FieldGroup(); + private Object editedItemId = null; + + private boolean isDetached = false; + + private HashSet uneditableProperties = new HashSet(); + + /** + * Constructs a new editor row bound to a particular container. + * + * @param container + * the container this editor row is bound to + */ + EditorRow(Container container) { + this.container = container; + } + + /** + * Checks whether the editor row feature is enabled for the grid or not. + * + * @return true iff the editor row feature is enabled for the + * grid + * @see #getEditedItemId() + */ + public boolean isEnabled() { + checkDetached(); + return isEnabled; + } + + /** + * Sets whether or not the editor row feature is enabled for the grid. + * + * @param isEnabled + * true to enable the feature, false + * otherwise + * @throws IllegalStateException + * if an item is currently being edited + * @see #getEditedItemId() + */ + public void setEnabled(boolean isEnabled) throws IllegalStateException { + checkDetached(); + if (getEditedItemId() != null) { + throw new IllegalStateException("Cannot disable the editor row " + + "while an item (" + getEditedItemId() + + ") is being edited."); + } + this.isEnabled = isEnabled; + } + + /** + * Gets the field group that is backing this editor row. + * + * @return the backing field group + */ + public FieldGroup getFieldGroup() { + checkDetached(); + return fieldGroup; + } + + /** + * Sets the field group that is backing this editor row. + * + * @param fieldGroup + * the backing field group + */ + public void setFieldGroup(FieldGroup fieldGroup) { + checkDetached(); + this.fieldGroup = fieldGroup; + if (editedItemId != null) { + this.fieldGroup.setItemDataSource(container.getItem(editedItemId)); + } + } + + /** + * 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. + *

+ * Note: This is a pass-through call to the backing field group. + * + * @param propertyId + * The property id to bind to. Must be present in the field + * finder + * @param fieldType + * The type of field that we want to create + * @throws BindException + * If the field could not be created + * @return The created and bound field. Can be any type of {@link Field}. + */ + public > T buildAndBind(Object propertyId, + Class fieldComponent) throws BindException { + checkDetached(); + return fieldGroup.buildAndBind(null, propertyId, fieldComponent); + } + + /** + * 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 #editItem(Object)}. + *

+ * This method also adds validators when applicable. + *

+ * Note: This is a pass-through call to the backing field group. + * + * @param field + * The field to bind + * @param propertyId + * The propertyId to bind to the field + * @throws BindException + * If the property id is already bound to another field by this + * field binder + */ + public void bind(Object propertyId, Field field) throws BindException { + checkDetached(); + fieldGroup.bind(field, propertyId); + } + + /** + * Sets the field factory for the {@link FieldGroup}. The field factory is + * only used when {@link FieldGroup} creates a new field. + *

+ * Note: This is a pass-through call to the backing field group. + * + * @param fieldFactory + * The field factory to use + */ + public void setFieldFactory(FieldGroupFieldFactory factory) { + checkDetached(); + fieldGroup.setFieldFactory(factory); + } + + /** + * Gets the field component that represents a property. + *

+ * If the property is not yet bound to a field, it will be bound during this + * call. Otherwise the previously bound field will be used. + * + * @param propertyId + * the property id of the property for which to find the field + * @see #setPropertyUneditable(Object) + */ + public Field getField(Object propertyId) { + checkDetached(); + + final Field field; + if (fieldGroup.getUnboundPropertyIds().contains(propertyId)) { + field = fieldGroup.buildAndBind(propertyId); + } else { + field = fieldGroup.getField(propertyId); + } + + if (field != null) { + boolean readonly = fieldGroup.isReadOnly() + || field.getPropertyDataSource().isReadOnly() + || !isPropertyEditable(propertyId); + field.setReadOnly(readonly); + } + + return field; + } + + /** + * Sets a property editable or not. + *

+ * In order for a user to edit a particular value with a Field, it needs to + * be both non-readonly and editable. + *

+ * The difference between read-only and uneditable is that the read-only + * state is propagated back into the property, while the editable property + * is internal metadata for the editor row. + * + * @param propertyId + * the id of the property to set as editable state + * @param editable + * whether or not {@code propertyId} chould be editable + */ + public void setPropertyEditable(Object propertyId, boolean editable) { + checkDetached(); + checkPropertyExists(propertyId); + if (editable) { + uneditableProperties.remove(propertyId); + } else { + uneditableProperties.add(propertyId); + } + } + + /** + * Checks whether a property is uneditable or not. + *

+ * This only checks whether the property is configured as uneditable in this + * editor row. The property's or field's readonly status will ultimately + * decide whether the value can be edited or not. + * + * @param propertyId + * the id of the property to check for editable status + * @return true iff the property is editable according to this + * editor row + */ + public boolean isPropertyEditable(Object propertyId) { + checkDetached(); + checkPropertyExists(propertyId); + return !uneditableProperties.contains(propertyId); + } + + /** + * Commits all changes done to the bound fields. + *

+ * Note: This is a pass-through call to the backing field group. + * + * @throws CommitException + * If the commit was aborted + */ + public void commit() throws CommitException { + checkDetached(); + fieldGroup.commit(); + } + + /** + * Discards all changes done to the bound fields. + *

+ * Note: This is a pass-through call to the backing field group. + */ + public void discard() { + checkDetached(); + fieldGroup.discard(); + } + + /** + * Internal method to inform the editor row that it is no longer attached to + * a Grid. + */ + void detach() { + checkDetached(); + isDetached = true; + } + + /** + * Sets an item as editable. + * + * @param itemId + * the id of the item to edit + * @throws IllegalStateException + * if the editor row is not enabled + * @throws IllegalArgumentException + * if the {@code itemId} is not in the backing container + * @see #setEnabled(boolean) + */ + public void editItem(Object itemId) throws IllegalStateException, + IllegalArgumentException { + checkDetached(); + + if (!isEnabled()) { + throw new IllegalStateException("This " + + getClass().getSimpleName() + " is not enabled"); + } + + Item item = container.getItem(itemId); + if (item == null) { + throw new IllegalArgumentException("Item with id " + itemId + + " not found in current container"); + } + + fieldGroup.setItemDataSource(item); + editedItemId = itemId; + } + + /** + * Gets the id of the item that is currently being edited. + * + * @return the id of the item that is currently being edited, or + * null if no item is being edited at the moment + */ + public Object getEditedItemId() { + checkDetached(); + return editedItemId; + } + + /** + * Gets a collection of all fields represented by this editor row. + *

+ * All non-editable fields (either readonly or uneditable) are in read-only + * mode. + * + * @return a collection of all the fields represented by this editor row + */ + Collection> getFields() { + checkDetached(); + + /* + * Maybe this isn't the best idea, however. Maybe the components should + * always be transferred over the wire, to increase up-front load-time + * and decrease on-demand load-time. + */ + if (!isEnabled()) { + return Collections.emptySet(); + } + + for (Object propertyId : fieldGroup.getUnboundPropertyIds()) { + fieldGroup.buildAndBind(propertyId); + } + + /* + * We'll collect this ourselves instead of asking fieldGroup.getFields() + * because we might have marked something as uneditable even though it + * might not read-only. + */ + ArrayList> fields = new ArrayList>(); + for (Object propertyId : container.getContainerPropertyIds()) { + Field field = getField(propertyId); + if (field != null) { + fields.add(field); + } + } + + return fields; + } + + private void checkDetached() throws IllegalStateException { + if (isDetached) { + throw new IllegalStateException("The method cannot be " + + "processed as this " + getClass().getSimpleName() + + " has become detached."); + } + } + + private void checkPropertyExists(Object propertyId) { + if (!container.getContainerPropertyIds().contains(propertyId)) { + throw new IllegalArgumentException("Property with id " + propertyId + + " is not in the current Container"); + } + } +} diff --git a/server/src/com/vaadin/ui/components/grid/Grid.java b/server/src/com/vaadin/ui/components/grid/Grid.java index 3c115f9241..f6a1231f43 100644 --- a/server/src/com/vaadin/ui/components/grid/Grid.java +++ b/server/src/com/vaadin/ui/components/grid/Grid.java @@ -257,6 +257,8 @@ public class Grid extends AbstractComponent implements SelectionChangeNotifier, private final GridHeader header = new GridHeader(this); private final GridFooter footer = new GridFooter(this); + private EditorRow editorRow; + private static final Method SELECTION_CHANGE_METHOD = ReflectTools .findMethod(SelectionChangeListener.class, "selectionChange", SelectionChangeEvent.class); @@ -420,6 +422,15 @@ public class Grid extends AbstractComponent implements SelectionChangeNotifier, datasource = container; + /* + * This is null when this method is called the first time in the + * constructor + */ + if (editorRow != null) { + editorRow.detach(); + } + editorRow = new EditorRow(datasource); + // // Adjust sort order // @@ -1302,6 +1313,16 @@ public class Grid extends AbstractComponent implements SelectionChangeNotifier, } } + componentList.addAll(getEditorRow().getFields()); return componentList.iterator(); } + + /** + * Gets the editor row configuration object. + * + * @return the editor row configuration object + */ + public EditorRow getEditorRow() { + return editorRow; + } } diff --git a/server/tests/src/com/vaadin/tests/server/component/grid/EditorRowTests.java b/server/tests/src/com/vaadin/tests/server/component/grid/EditorRowTests.java new file mode 100644 index 0000000000..36c541c99c --- /dev/null +++ b/server/tests/src/com/vaadin/tests/server/component/grid/EditorRowTests.java @@ -0,0 +1,234 @@ +/* + * Copyright 2000-2014 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 org.junit.Before; +import org.junit.Test; + +import com.vaadin.data.Item; +import com.vaadin.data.util.IndexedContainer; +import com.vaadin.ui.Field; +import com.vaadin.ui.TextField; +import com.vaadin.ui.components.grid.EditorRow; +import com.vaadin.ui.components.grid.Grid; + +public class EditorRowTests { + + private static final Object PROPERTY_NAME = "name"; + private static final Object PROPERTY_AGE = "age"; + private static final Object ITEM_ID = new Object(); + + private Grid grid; + private EditorRow row; + + @Before + @SuppressWarnings("unchecked") + public void setup() { + 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("Some Valid Name"); + item.getItemProperty(PROPERTY_AGE).setValue(Integer.valueOf(25)); + + grid = new Grid(container); + row = grid.getEditorRow(); + } + + @Test + public void initAssumptions() throws Exception { + assertNotNull(row); + assertFalse(row.isEnabled()); + assertNull(row.getEditedItemId()); + assertNotNull(row.getFieldGroup()); + } + + @Test + public void setEnabled() throws Exception { + assertFalse(row.isEnabled()); + row.setEnabled(true); + assertTrue(row.isEnabled()); + } + + @Test + public void setDisabled() throws Exception { + assertFalse(row.isEnabled()); + row.setEnabled(true); + row.setEnabled(false); + assertFalse(row.isEnabled()); + } + + @Test + public void setReEnabled() throws Exception { + assertFalse(row.isEnabled()); + row.setEnabled(true); + row.setEnabled(false); + row.setEnabled(true); + assertTrue(row.isEnabled()); + } + + @Test(expected = IllegalStateException.class) + public void detached() throws Exception { + EditorRow oldEditorRow = row; + grid.setContainerDataSource(new IndexedContainer()); + oldEditorRow.isEnabled(); + } + + @Test + public void propertyUneditable() throws Exception { + row.setPropertyEditable(PROPERTY_NAME, false); + } + + @Test(expected = IllegalArgumentException.class) + public void nonexistentPropertyUneditable() throws Exception { + row.setPropertyEditable(new Object(), false); + } + + @Test(expected = IllegalStateException.class) + public void disabledEditItem() throws Exception { + row.editItem(ITEM_ID); + } + + @Test + public void editItem() throws Exception { + startEdit(); + assertEquals(ITEM_ID, row.getEditedItemId()); + } + + @Test(expected = IllegalArgumentException.class) + public void nonexistentEditItem() throws Exception { + row.setEnabled(true); + row.editItem(new Object()); + } + + @Test + public void getField() throws Exception { + startEdit(); + + assertNotNull(row.getField(PROPERTY_NAME)); + } + + @Test + public void getFieldWithoutItem() throws Exception { + row.setEnabled(true); + assertNull(row.getField(PROPERTY_NAME)); + } + + @Test + public void getFieldAfterReSettingFieldAsEditable() throws Exception { + startEdit(); + + row.setPropertyEditable(PROPERTY_NAME, false); + row.setPropertyEditable(PROPERTY_NAME, true); + assertNotNull(row.getField(PROPERTY_NAME)); + } + + @Test + public void isEditable() { + assertTrue(row.isPropertyEditable(PROPERTY_NAME)); + } + + @Test + public void isUneditable() { + row.setPropertyEditable(PROPERTY_NAME, false); + assertFalse(row.isPropertyEditable(PROPERTY_NAME)); + } + + @Test + public void isEditableAgain() { + row.setPropertyEditable(PROPERTY_NAME, false); + row.setPropertyEditable(PROPERTY_NAME, true); + assertTrue(row.isPropertyEditable(PROPERTY_NAME)); + } + + @Test + public void isUneditableAgain() { + row.setPropertyEditable(PROPERTY_NAME, false); + row.setPropertyEditable(PROPERTY_NAME, true); + row.setPropertyEditable(PROPERTY_NAME, false); + assertFalse(row.isPropertyEditable(PROPERTY_NAME)); + } + + @Test(expected = IllegalArgumentException.class) + public void isNonexistentEditable() { + row.isPropertyEditable(new Object()); + } + + @Test(expected = IllegalArgumentException.class) + public void setNonexistentUneditable() { + row.setPropertyEditable(new Object(), false); + } + + @Test(expected = IllegalArgumentException.class) + public void setNonexistentEditable() { + row.setPropertyEditable(new Object(), true); + } + + @Test + public void customBinding() { + startEdit(); + + TextField textField = new TextField(); + row.bind(PROPERTY_NAME, textField); + assertSame(textField, row.getField(PROPERTY_NAME)); + } + + @Test(expected = IllegalStateException.class) + public void disableWhileEditing() { + startEdit(); + row.setEnabled(false); + } + + @Test + public void fieldIsNotReadonly() { + startEdit(); + + Field field = row.getField(PROPERTY_NAME); + assertFalse(field.isReadOnly()); + } + + @Test + public void fieldIsReadonlyWhenFieldGroupIsReadonly() { + startEdit(); + + row.getFieldGroup().setReadOnly(true); + Field field = row.getField(PROPERTY_NAME); + assertTrue(field.isReadOnly()); + } + + @Test + public void fieldIsReadonlyWhenPropertyIsNotEditable() { + startEdit(); + + row.setPropertyEditable(PROPERTY_NAME, false); + Field field = row.getField(PROPERTY_NAME); + assertTrue(field.isReadOnly()); + } + + private void startEdit() { + row.setEnabled(true); + row.editItem(ITEM_ID); + } +} -- 2.39.5