]> source.dussan.org Git - vaadin-framework.git/commitdiff
Server-side editor row (#13334)
authorHenrik Paul <henrik@vaadin.com>
Mon, 18 Aug 2014 09:43:03 +0000 (12:43 +0300)
committerHenrik Paul <henrik@vaadin.com>
Wed, 27 Aug 2014 06:48:38 +0000 (06:48 +0000)
Change-Id: Ia84c8f0a00549318e35e2c844b6ec6c419cfa4f3

server/src/com/vaadin/ui/components/grid/EditorRow.java [new file with mode: 0644]
server/src/com/vaadin/ui/components/grid/Grid.java
server/tests/src/com/vaadin/tests/server/component/grid/EditorRowTests.java [new file with mode: 0644]

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 (file)
index 0000000..5eb3815
--- /dev/null
@@ -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<Object> uneditableProperties = new HashSet<Object>();
+
+    /**
+     * 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 <code>true</code> 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
+     *            <code>true</code> to enable the feature, <code>false</code>
+     *            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.
+     * <p>
+     * <em>Note:</em> 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 extends Field<?>> T buildAndBind(Object propertyId,
+            Class<T> 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)}.
+     * <p>
+     * This method also adds validators when applicable.
+     * <p>
+     * <em>Note:</em> 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.
+     * <p>
+     * <em>Note:</em> 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.
+     * <p>
+     * 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.
+     * <p>
+     * In order for a user to edit a particular value with a Field, it needs to
+     * be both non-readonly and editable.
+     * <p>
+     * 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.
+     * <p>
+     * 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 <code>true</code> 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.
+     * <p>
+     * <em>Note:</em> 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.
+     * <p>
+     * <em>Note:</em> 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
+     *         <code>null</code> 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.
+     * <p>
+     * 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<Field<?>> 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<Field<?>> fields = new ArrayList<Field<?>>();
+        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");
+        }
+    }
+}
index 3c115f92415ad0dfb4034a5952b549aad1b96a6b..f6a1231f43b553dd74be7c3c0b7aed1819671673 100644 (file)
@@ -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 (file)
index 0000000..36c541c
--- /dev/null
@@ -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);
+    }
+}